问题描述
今天在写代码的时候遇到一个很奇怪的现象,先看下面两段代码
func push(a []int, v int) {
a[1] = 2
a = append(a, v)
}
func main() {
a := []int{0, 1, 2}
push(a, 3)
fmt.Println(a)
}
结果:[0 2 2]
func push(a []int, v int) {
a = append(a, v)
a[1] = 2
}
func main() {
a := []int{0, 1, 2}
push(a, 3)
fmt.Println(a)
}
结果:[0 1 2]
乍一看这两段代码几乎一模一样,唯一的不同在于push函数中两行代码的顺序不一致
这两段代码中有两个问题
- 为什么第一段代码中赋值语句起到作用,append没有起到作用
- 为什么第二段代码中的赋值语句和append都没有起到作用
问题分析
第一个问题:为什么第一段代码中赋值语句起到作用,append没有起到作用
首先我们要清楚Go语言中不存在引用传递,即这里的a []int是值传递,我们不妨输出一下a的地址
可以看到函数内外的a并不是同一个切片,那么既然不是同一个切片,为什么在第一段代码中,修改了函数内的a,函数外的a也会发生改变呢?
这里我们需要了解go语言中切片是如何实现的
可以看下图,go语言中的切片实际上是对底层数组的一个view
切片由三部分组成,分别是指向底层数组的指针ptr,切片的长度len,底层数组的长度cap
由此就可以解释为何在第一段代码中修改函数内的切片,函数外的切片也会发生改变,两个切片虽然地址不同,但是它们两个的值是相同的,也就是说它们两个内部的ptr是相同的都指向同一个底层数组,所以修改其中一个,另外一个也就会随之改变。同理,在函数内append时,函数内部的切片len增加了,但由于是值传递,所以函数外部的切片len没有改变,因此函数内部的切片append不会引起函数外部的切片改变。
第二个问题:为什么第二段代码中的赋值语句和append都没有起到作用
首先关于append为什么没有起到作用,在上面已经解释过了,这里我们重点关注为什么赋值语句也没有起到作用
原因只有一句话:切片在添加元素时如果超越cap,那么就不再是对原数组的view,系统会重新分配更大的底层数组
继续分析之前的代码,在输出地址的基础上再输出切片的len和cap
可以看到,在执行append之前,切片的len等于cap,执行append后,切片的长度会超过cap,此时系统会重新分配更大的数组。观察输出可以发现,执行完append后切片的cap发生了变化,与我们的设想一致,系统重新分配了一个更大的数组给切片,切片的ptr指针指向了另一个数组,与函数外的切片不再指向同一个数组,因此在函数内修改切片的值的时候对函数外的切片就不会产生影响了
更进一步,我们将切片赋予一个较大的cap,使函数内的切片再执行append后len不会超过cap,观察此时的函数外的切片是否会发生变化
可以看到此时函数内的赋值语句成功修改了函数外的切片的值,因为此时函数内的切片执行append后,切片的len没有超过cap,并不会分配新数组,因此后面再执行赋值语句时修改的还是函数外的数组