Go 语言切片的值传递

 · 5 min read 

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],并没有被追加元素,这显然表现出了值传递的效果。至此,人们对于切片作为函数参数传递时,是值传递还是引用传递,就有了分歧。这里我可以明确告知,本质就是值传递。

值传递的本质

切片作为函数参数传递时,传递的其实是切片本身的副本,而非原始切片的内存地址。切片底层结构包含了三个字段:

  1. array:指向底层数组的指针;
  2. len:当前元素的个数;
  3. cap:底层数组的容量。 当切片作为参数传递时,其实传递的是这三个字段的副本,而非整个底层数组的副本。函数修改切片元素时,由于是同一份底层数组的指针,所以会直接影响原切片,但是修改切片元信息(如扩容)是不会影响原切片的。

所以上面例子中,直接修改切片第一项元素值s[0] = 100时,由于切片副本的 array字段与原切片指向同一底层数组,所以修改元素影响了原数据。

而在给切片副本扩容时,即上例中 s = append(s, 4),由于扩容会修改底层数组地址,此时 sarray字段指向了新的地址,所以原切片 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,以及最后一个零值。

#Go