前景提要
今天做项目的时候,最终的结果死活不对。仔细研究之后才发现,原来是踩到slice作为函数参数的一个坑,当时的代码逻辑大概像这样:
func main() { input := make([]int, 0) fmt.Println("Origianl:", input) dealData(input) fmt.Println("Output:", input) } func dealData(input []int) { for i := 0; i < 10; i++ { input = append(input, i) } }
首先在函数外部创建了一个切片,而后将切片作为函数参数传入数据处理函数中。在这个函数中,会对原切片进行数据填充。
这种实现的本来目的是获得一个从0到9的切片,结果却是下面这个样子:
也就是说input这个切片进入这个函数没有一点用处。
回过头来想想,我当时之所以要这么写,是因为从某个地方看到“golang的切片可以作为函数参数,等同于引用传递,在函数内修改会影响外部切片”。
我大概知道问题出在哪了,于是写了下面这个函数:
func main() { input := make([]int, 10) fmt.Println("Origianl:", input) dealData(input) fmt.Println("Output:", input) } func dealData(input []int) { for i := 0; i < 10; i++ { input[i] = i } }
这个时候的输出就是:
那么这到底是什么问题呢?为什么我可以修改原切片元素,但是append就不行?
问题解析
其实简单来说,如果只是进行元素的修改,将切片作为参数没有一点问题,但只在函数中对原切片进行了数据的增加等操作,就会造成上述的现象。
归根究底,还是得说会golang切片(slice)的底层结构:
type slice struct { array unsafe.Pointer len int cap int }
切片其实主要的构成元素有三个:指向底层数组的指针ptr、当前数组中元素个数len、底层数组可容纳的最大元素个数cap。
当我们将切片作为函数形参传递时,其实传递的就是这三个主要元素,由于形参指向的底层数组地址和外部切片指向的底层数组相同,因此在函数内部对切片进行的修改都会反应到外部。
但是,当我们进行append操作的时候,情况就不一样了。
**append函数对原切片进行填充,如果原切片容量足够,没有达到扩容的阈值,就在原切片的底层数组上进行数组填充;如果切片容量不够,就进行扩容,并将原数据复制过去再进行数据填充。而后者就会让原切片的底层数组地址发生变动,**这也是为什么,append函数返回的也是一个切片,而且一般使用它的时候都是这样的:
arr = append(arr, "h")
这是从语法层面防止用户遗忘了这个特性。
让我们来看一组代码:
func main() { input := make([]int, 0) fmt.Println("Origianl:", input) fmt.Printf("Origianl address %p %p;\n", &input, input) dealData(input) fmt.Println("Output:", input) fmt.Printf("Output address %p %p;\n", &input, input) } func dealData(input []int) { for i := 0; i < 10; i++ { input = append(input, i) fmt.Printf("i = %d ,len = %d ,cap = %d ,Temp address is %p %p\n", i, len(input), cap(input), &input, input) } }
其中 fmt.Printf(“Origianl address %p %p;\n”, &input, input) 这一行代码会先打出切片的内存地址,再打出切片的底层数组地址。
好好看看这组代码的输出:
Origianl: [] Origianl address 0xc000004078 0xfc2438; i = 0 ,len = 1 ,cap = 1 ,Temp address is 0xc0000040c0 0xc000012098//第一次扩容 i = 1 ,len = 2 ,cap = 2 ,Temp address is 0xc0000040c0 0xc0000120d0//第二次 i = 2 ,len = 3 ,cap = 4 ,Temp address is 0xc0000040c0 0xc00000a2a0 i = 3 ,len = 4 ,cap = 4 ,Temp address is 0xc0000040c0 0xc00000a2a0 i = 4 ,len = 5 ,cap = 8 ,Temp address is 0xc0000040c0 0xc000010280/第三次 i = 5 ,len = 6 ,cap = 8 ,Temp address is 0xc0000040c0 0xc000010280 i = 6 ,len = 7 ,cap = 8 ,Temp address is 0xc0000040c0 0xc000010280 i = 7 ,len = 8 ,cap = 8 ,Temp address is 0xc0000040c0 0xc000010280 i = 8 ,len = 9 ,cap = 16 ,Temp address is 0xc0000040c0 0xc00001a180//第四次 i = 9 ,len = 10 ,cap = 16 ,Temp address is 0xc0000040c0 0xc00001a180 Output: [] Output address 0xc000004078 0xfc2438;
我想大家已经发现了吧?
从进入函数之后append第一个元素之后,切片的底层数组地址就已经跟函数外部切片的底层数组地址不一致了!
这是因为append导致了扩容,并且这种扩容现象在之后发生了几次,从而造成底层数组地址持续变动。
总结
虽然踩了坑,但是对切片的理解也更加深入了。
回到题目,如果想解决前景提要中遇到的问题应该怎么办?
有两种简单的方式:一是传入切片指针,而不是切片本身;二是让函数返回一个切片,对外部切片进行赋值,而不是直接将外部切片作为参数传递进去。
如果非要传入切片,那就在外部申请足够大空间,避免底层数组地址的变动,但是这个方法实际长场景不好操作,很多时候并不知道需要申请多大的空间。
推荐阅读
https://halfrost.com/go_slice/
,而不是直接将外部切片作为参数传递进去**。
如果非要传入切片,那就在外部申请足够大空间,避免底层数组地址的变动,但是这个方法实际长场景不好操作,很多时候并不知道需要申请多大的空间。