Go语言中的切片类型,在作为函数参数传递时,是“值传递”还是“引用传递”? 答案正如标题所言,是值传递。但是有时候的实际表现,会让人误以为是引用传递。本文详细分析一下这个问题。
引用传递的效果
之所以很多人觉得切片作为函数参数传递时是引用传递,原因就是在函数内部修改参数时,影响了原切片的值,这里有个例子:
func changeSlice(s []int){
s[0] = 100
}
func main(){
s1 := []int{1, 2, 3}
changeSlice(s1)
fmt.Print(s1) // [100 2 3]
}
代码中,切片 s1
作为参数传递给函数 changeSlice
,函数内部修改了切片的第一项值,最后打印切片 s1
,发现确实是被修改了。这妥妥的就是引用传递的效果,但实际真的如此吗?来改一下函数changeSlice
:
func changeSlice(s []int){
s = append(s, 4)
}
此时再打印 s1
,就会发现其值还是 [1 2 3]
,并没有被追加元素,这显然表现出了值传递的效果。至此,人们对于切片作为函数参数传递时,是值传递还是引用传递,就有了分歧。这里我可以明确告知,本质就是值传递。
值传递的本质
切片作为函数参数传递时,传递的其实是切片本身的副本,而非原始切片的内存地址。切片底层结构包含了三个字段:
- array:指向底层数组的指针;
- len:当前元素的个数;
- cap:底层数组的容量。 当切片作为参数传递时,其实传递的是这三个字段的副本,而非整个底层数组的副本。函数修改切片元素时,由于是同一份底层数组的指针,所以会直接影响原切片,但是修改切片元信息(如扩容)是不会影响原切片的。
所以上面例子中,直接修改切片第一项元素值s[0] = 100
时,由于切片副本的 array
字段与原切片指向同一底层数组,所以修改元素影响了原数据。
而在给切片副本扩容时,即上例中 s = append(s, 4)
,由于扩容会修改底层数组地址,此时 s
的 array
字段指向了新的地址,所以原切片 s1
没有被影响。
案例分析
上面说到,扩容会改变底层数组地址,此时原数据不会被影响。那如果在 append
时,切片的容量还够时呢?例如:
func changeSlice(s []int) {
s = append(s, 4)
}
func main() {
s1 := make([]int, 3, 5)
s1[0], s1[1], s1[2] = 1, 2, 3
changeSlice(s1)
fmt.Print(s1)
}
代码中使用 make
初始化了一个切片,指定了长度是 3,容量是 5,当三个长度被填充后,使用函数给切片追加第 4 个元素,此时 s1
的值是多少呢?
按照之前说的,只有扩容时才会改变底层数组,但此时追加一个元素时,容量还够,无需扩容,所以改变的还是原底层数组,所以 s1
应该是 [1 2 3 4]
。但其实最后 s1
的值还是 [1 2 3]
。
作为初学者,我就在这里困惑过,为什么不扩容还是不能修改原切片数据呢?原因其实是我们忽略了一个字段,那就是切片长度 len
。此例中修改的确实还是同一份底层数组,但是原切片中的长度还是 3,它的可见范围还是前 3 个元素,所以 s1
最后的值还是 [1 2 3]
。其实 s1
的底层数组已经被修改了,证明:
fmt.Print(s1[:5]) // [1 2 3 4 0]
使用 s1[:5]
截取底层数组所有元素,即可以看到被追加的元素 4
,以及最后一个零值。