golang中令人迷惑的切片与函数参数传递
研究下切片作为函数参数时到底发生了什么
简单测试一下
先用一个数组试一下:
1 | func main() { |
看来数组是拷贝的
把数组变成切片
1 | func main() { |
数组被改变了,这说明切片是引用传递的。
事情没那么简单
那么问题来了,既然切片都能按引用传递,那怎么数组还是按照值传递的呢?
再来试一下这个:
1 | func main() { |
数组的地址和指针的地址不一样啊。怎么回事?
这是因为切片和数组的构造其实完全不一样,切片其实是一个包含数组信息的结构体:
1 | // SliceHeader is the runtime representation of a slice. |
切片里面包含数组位置,切片长度和切片容量(数组大小),所以我们可以解释刚才的参数传递行为:
golang其实就是完全值传递的,只不过由于切片的特殊结构,里面保存了原数组的指针,所以我们才能在函数内修改原数组。
同时这也解释了另外一个问题:为什么对切片append()
需要用返回值覆盖原来切片?因为append之后切片内部的容量和长度信息变了,而golang为我们隐藏了切片的内部实现,所以我们不能像Java的List那样使用一个对象和属性来变更信息,只能覆盖原切片。
下面再看一个例子:
1 | func main() { |
对切片进行了扩容,再改变切片的话,原来的切片还是没变。这是因为我们原来的切片来自一个长度只有5的数组,所以切片的容量就是数组的长度,想要扩容就只能复制原来的数组了
扩容两次再试试
1 | func changeSlice(slice []int) { |
可以看到,第二次扩容后,slice2的改变能影响到slice1。因为切片第一次扩容后并不是简单的增加了一位。如果,每次append都只+1的话,那岂不是效率很慢,而且会占用很多内存空间。所以golang对切片扩容有其他的规律。具体请看参考资料。
1 | func changeSlice(slice []int) { |
如上,每次扩容切片容量翻倍。
总结
所以切片到底是”传值”还是”传引用”,完全要看你对切片的操作有没有导致扩容。
如果真的要再一个函数里面操作切片的话,不希望改变原切片,可以使用copy(a,b)拷贝一下。
如果希望改变原切片,要么保证你的操作不会引起扩容,要么可以使用闭包。如果以上两个都不行,也可以选择传递指针。
1 | // 用指针修复扩容导致的切片引用失效 |
参考资料
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array-and-slice/