切片与数组
Go中的切片是在数组之上的抽象数据类型。众所周知,数组类型定义了元素类型和长度,即长度是数组类型的一部分,是固定不变的。 在Go中,数组是值语义的,而不是类似与C中数组是指向第一个元素的指针。当一个数组被作为参数传递时,实际上会复制整个数组;为了避免复制数组,可以传递一个指向数组的指针。 由于数组长度是固定的,导致了数组不够灵活,在Go中数组用得并不是很多,用得更广泛得是切片。切片是基于数组构建的,但是比数组更加强大——切片没有给定固定的长度。因此在创建切片的时候也可以不用指定长度:
slice1 := []int{1, 2, 3}
//使用make创建切片
var slice2 = make([]int, 5)
当要向切片的末尾追加数据时,可以使用append
:
slice1 = append(s1, 7, 8, 9)
关于切片的更多内容,可以参考Go切片:用法与本质
append 后的行为不一致
值得注意的是,在该文章中提到append
在需要跟多的容量时,会创建一个新的、更大容量的切片,然后将原有切片的内容复制到新的切片。
这意味着,如果有两个切片指向了同一个数组,然后其中对其中一个切片进行append后,如果不需要更大的容量,再修改其中一个切片的内容,这两个切片的内容是相同的;但是如果需要更大的容量时,两个切片的内容是不同的:
arr := [6]int{1, 2, 3, 4, 5}
//用同一个数组创建了两个切片
s1 := arr[:5]
s2 := arr[:5]
//打印数组和切片,可以看到数组和两个切片的内容是一致的
fmt.Println(arr, s1, s2)
//向其中一个切片追加数据,然后修改其中一个切片的数据
s1 = append(s1, 6)
s1[0] = 10
fmt.Println(arr, s1, s2)
s1 = append(s1, 7)
s1[0] = 0
fmt.Println(arr, s1, s2)
输出结果为:
[1 2 3 4 5 0] [1 2 3 4 5] [1 2 3 4 5] [10 2 3 4 5 6] [10 2 3 4 5 6] [10 2 3 4 5] [10 2 3 4 5 6] [0 2 3 4 5 6 7] [10 2 3 4 5]
两次都是执行了append后修改第一个数据,但是第一次数据是相同的,第二次数据是不同的。 说明在第二次append时确实是创建了新的数组,但是两次几乎一样的操作带来不同的效果,在实际的编码过程中是需要注意的一个问题。如果只是在同一个函数内这样的问题还比较好发现,如果是换成函数就不同了:
func testAppend(data *[]int) {
*data = append(*data, 1)
(*data)[0] = (*data)[0] + 1
}
func main() {
arr := [6]int{1, 2, 3, 4, 5}
//用同一个数组创建了两个切片
s1 := arr[:5]
s2 := arr[:5]
//打印数组和切片,可以看到数组和两个切片的内容是一致的
fmt.Println(arr, s1, s2)
//向其中一个切片追加数据,然后修改其中一个切片的数据
// s1 = append(s1, 6)
// s1[0] = 10
testAppend(&s1)
fmt.Println(arr, s1, s2)
// s1 = append(s1, 7)
// s1[0] = 0
testAppend(&s1)
fmt.Println(arr, s1, s2)
}
输出结果:
[1 2 3 4 5 0] [1 2 3 4 5] [1 2 3 4 5] [2 2 3 4 5 1] [2 2 3 4 5 1] [2 2 3 4 5] [2 2 3 4 5 1] [3 2 3 4 5 1 1] [2 2 3 4 5]
当然,使用切片指针作为参数的情况应该很少,而且还要在函数中调用appen追加数据。这种情况应该很少会发生,但是既然可能出现这种问题,就应该在代码中尽量避免。
而如果函数传递的参数是切片本身,而不是指向切片的指针,此时在函数内部如果调用了append,将会导致更难排查的问题:
func testAppend(data []int) {
data = append(data, 1)
data[0] = data[0] + 1
}
func main() {
arr := [6]int{1, 2, 3, 4, 5}
s1 := arr[:5]
s2 := arr[:6]
fmt.Println(arr, s1, s2)
testAppend(s1)
fmt.Println(arr, s1, s2)
// s1 = append(s1, 7)
// s1[0] = 0
testAppend(s2)
fmt.Println(arr, s1, s2)
}
输出结果:
[1 2 3 4 5 0] [1 2 3 4 5] [1 2 3 4 5 0] [2 2 3 4 5 1] [2 2 3 4 5] [2 2 3 4 5 1] [2 2 3 4 5 1] [2 2 3 4 5] [2 2 3 4 5 1]
如果说函数testAppend
的目的是要在所给切片末尾追加数据,并且修改其中一部分数据,此时第一次调用达到了想要的目的,第二次调用由于后续的修改是在新的切片上进行的,无法达到预期的效果
如果想要保证在函数确实能起作用,需要传递向之前的代码将指向切片的指针作为参数,但是就会出现之前的代码所说的问题
结论
不要在函数内部对参数的切片执行append
操作,否则可能会达不到预期的效果。不管了将切片作为参数,还是将切片的指针作为参数,都可能导致行为的不一致