Go排坑之路:切片的传递方式(切片的陷阱)

开始

有一个函数foo修改切片元素的内容。很显然,上面这段代码修改成功了。此时我们就会默认切片是引用传递,陷入一个陷阱。

第一个陷阱

我们发现当我们在foo函数内,为切片a追加元素后,再修改切片内元素的内容,外部输出,不仅切片的长度没有改变,连切片的内容丢没有被修改。

分析

切片底层是数组,数组长度不可变。当切片长度发生变化后,原有数组不满足需求,系统会重新申请一块连续的内存,作为新数组的地址。而切片的地址就会发生变化,之后对切片的修改都不会影响原来的数据。

验证

可以看到在append之后输出的地址的确变了。

第二个陷阱

根据上面的分析,我们只需要申明切片时让cap足够大,满足在foo中追加变量后不重新申请内存的需求。那么是不是上面的代码会生效呢?

我们发现切片a的地址没有变化,a[0]元素也发生了变化,但是append上去的内容没有了,这是为什么呢?

怀疑

我们知道切片时候三部分组成的:datalencap。我怀疑虽然切片的地址没有发生变化,但是切片底层的有什么特别的逻辑吗?

使用unsafe.Pointer输出切片底层,分析问题。

分析

从输出内容我们可以发现切片底层data地址和cap容量没有发生变化。只有表示切片长度的len变长后,外部没有发生变化。

那我们修改外部切片的长度再进行观察。

验证

通过设置sh.Len = 10后再输出,我们可以看到,其实外部的切片数据已经发生了变化,只是受到切片长度的限制,并不能将所有元素都打印出来。

结论

传参的三个概念

  • 传值(值传递)
  • 传指针
  • 传引用(引用传递)
传值(值传递)

是指在调用函数时将实际参数拷贝一份传递到函数中,这样在函数中对参数进行修改不会影响到实际参数。这个简单不必赘述。

传指针

形参是指向实参地址的指针,当对形参的指向进行操作时,就相当于对实参本身进行操作。听起来比较绕是吧,我们来看个例子就知道了:

从打印结果中可以看到,bar函数中变量i是一个指针,指针的内容是 0xc000014108,存放指针变量i的地址为0xc000006030

传引用(引用传递)

是指在调用函数时将实际参数的地址传递到函数中,在函数中对参数所进行的修改,将影响实际参数。

假设以上面例子,如果在bar函数中打印指针变量&i的地址也是0xc000014108,那么我们就认为是引用传递。

但是Go官方说:Go函数传参只有值传递(包括传指针)

链接:https://golang.org/ref/spec#Calls

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution. The return parameters of the function are passed by value back to the calling function when the function returns.

返回切片的问题

我们可以看到切片本身就是一个地址0xc000108000main函数中存放切片的地址为0xc0000044a0,而foo函数中存放切片的地址为0xc000004500

所以当我们在foo函数内修改SliceHeaderLen或者Cap变量,并不会影响main函数中SliceHeaderLenCap