切片传参的幻觉 - 传引用
golang中函数的参数为切片时是传引用还是传值?对于这个问题,当你百度一轮过后,你会发现很大一部分人认为是传引用,通常他们会贴出下面这段代码进行佐证:
上面代码中,在main函数里边初始化一个切片变量slice,接着调用changeSlice函数,参数为切片变量slice。而函数changeSlice的主要处理逻辑是改变切片的第二个元素的值。下面我们看一下运行打印的结果:
slice:[0123]slice:[011123]
从输出结果我们看到,函数changeSlice内对切片的修改,main函数中的切片变量slice也跟着修改了。咋一看,这不就是引用传递的表现吗?
但事实上真的传引用吗?
理清三个重要概念
在探讨函数切片参数到底是以哪种方式传递时,我们先来理清下面三个重要的概念:
传值(值传递)
穿指针
传引用(引用传递)
传值(值传递)
是指在调用函数时将实际参数拷贝一份传递到函数中,这样在函数中对参数进行修改不会影响到实际参数。这个简单不必赘述。
传指针
形参是指向实参地址的指针,当对形参的指向进行操作时,就相当于对实参本身进行操作。听起来比较绕是吧,我们来看个例子就知道了:
上面代码中定义了一个变量 a,并把地址保存在指针变量pa里面;接着打印pa的值和pa的地址,然后调用modify函数,参数为指针变量pa;modify函数中首先打印形参p的值和p的地址,接着修改p的值为1。
我们打印输出的结果:
从输出结果中我们可以看到,这是一个指针的拷贝。指针pa 和 p 的值虽然相同,但是存放这两个指针的内存地址是不同的,因此这是两个不同的指针。
注意:任何存放在内存里的东西都有自己的地址,指针也不例外,它虽然指向别的数据,但是也有存放该指针的内存。
结合图来看相信会更清晰一点:
传引用(引用传递)
是指在调用函数时将实际参数的地址传递到函数中,在函数中对参数所进行的修改,将影响实际参数。
假设以上面demo为例子,如果在modify函数中打印指针变量p的地址也是0xc00000e028,那么我们就认为是引用传递
但这里我们不能用go来举例子,原因请接着往下看。
官方打假:Go函数传参只有值传递
看完传值、传指针、传引用的概念后,如果你坚持认为是传引用,好,大叔要在这里直接把你击垮。
根据Go官方文档声明:Go里面函数传参只有值传递一种方式。也就是说,在Go中,函数的传参只有传值一种方式。官方文档传送门
如果你还不服气,咱们直接看例子:
打印输出:
如果函数切片参数传的是引用,那么上面这个例子中,main函数中打印的切片slice的地址应该和changSlice函数中打印切片的地址一样的,但从输出结果来看并不是这样的。
因此在这里,我们可以非常肯定地说:有关Go函数中切片参数是传引用的说法是错误的,另外有关有关引用传递是针对slice、map、channel三种数据类型的说法也是错误的
切片参数本质还是传值
从上面的分析来看,切片参数传递的方式并非是传引用。反而极有可能是传指针,而在传指针那小节的分析中我们可以知道,传指针其实就是指针的拷贝,形参和实参是两个不同的指针,但是它们的值是一样的。本质上可以说还是传值。
那到底是不是这样的呢?大叔疑问句都出来了,说明90%是有可能的,接下我们就验证一下那剩下的10%。
slice的结构体
我们先来看一下切片的结构体是长啥样的:
切片,顾名思义就是数组切下来的一部分,其结构体包含了三部分,第一部分是指向底层数组的指针,其次是切片的大小len和切片的容量cap。(果然含有指针成员变量。)
上面的结构体看着有点变扭,不够直观,我们造个例子:一个数组 arr := [5]int{0,1,2,3,4},生成一个切片 slice := arr[1:4],最终得到的切片如下:
再看个例子:
func main() { arr := [5]int{0,1,2,3,4} slice1 := arr[1:4] slice2 := arr[2:5] // 打印一 fmt.Printf("arr %v, slice1 %v, slice2 %v arr addr: %p, slice1 addr: %p, slice2 addr: %p\n", arr, slice1, slice2, &arr, &slice1, &slice2) // 打印二 fmt.Printf("arr[2] addr: %p, slice1[1] addr: %p, slice2[0] addr: %p\n", &arr[2], &slice1[1], &slice2[0]) arr[2] =2222// 打印三 fmt.Printf("arr: %v, slice1: %v, slice2: %v\n", arr, slice1, slice2) slice1[1] =1111// 打印四 fmt.Printf("arr: %v, slice1: %v, slice2: %v\n", arr, slice1, slice2)}
上面代码中我们创建一个数组,并生成两个切片。打印它们的值和对应的地址。另外,修改数组或者切片的某个单元的值,观察数组和切片中单元的值的变化:
arr[01234],slice1[123],slice2[234]arraddr:0xc000014090,slice1addr:0xc00000c080,slice2addr:0xc00000c0a0arr[2]addr:0xc0000140a0,slice1[1]addr:0xc0000140a0,slice2[0]addr:0xc0000140a0arr:[01222234],slice1:[122223],slice2:[222234]arr:[01111134],slice1:[111113],slice2:[111134]
从打印一结果可以看出:创建的两个切片,它们各自拥有不同的地址
从打印二结果可以看出:切片元素slice1[1]、slice2[0] 与数组元素arr[2]有着同样的地址,说明这些切片共享着数组arr中的数据
打印三和打印四可以看出:修改数组和切片共同部分的数据,对两者都有直接影响,再次印证第二点的结论。
从上面的分析中我们可以知道,两个不同的切片之所以能相互影响,主要因素是切片内部的指针指向同一个数据源,且两个切片的指针指向的数据源中有交集。
再回到切片作为函数参数的问题上,因为Go里面函数传参只有值传递一种方式,所以当切片作为参数时,其实也是切片的拷贝,但是在拷贝的切片中,其包含的指针成员变量的值是一样的,也就是说它们指向的数据源是一样,因此在调用函数内修改形参能影响实参。
Go函数传值总结
通常,我们把在传值拷贝过程中,修改形参能直接修改实参的数据类型称为引用类型。
于是我们又可以这样总结:
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。因为拷贝的内容有时候是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;有的是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
这里要注意的是:引用类型和传引用是两个概念。
切片参数,修改形参一定影响实参吗
是的,看到这里,我们应该问自己一个问题:函数的切片参数,在函数内修改形参一定会影响外部的实参吗?(疑问句一出必然有鬼!)
我们来看个例子:
上面代码中,先是创建好一个长度为2,容量为3的空切片,然后给切片赋两个值,接着调用函数changeSlice,参数为赋完值的切片。函数changeSlice里面先是往切片中追加两个元素,然后再改变切片的第二个元素的值。
我们来看看打印结果:
slice:[01],addr:0xc00001a100funcs:[011134],addr:0xc000014090slice:[01],addr:0xc00001a100
从输出结果我们可以看到,函数内修改形参s后,外部的实参slice并没有跟着改变,而且注意到一点是,形参s和实参slice的指针成员变量的值是不一样的,也就是说它们指向的数据源不是同一个了。
这又是为什么呢?
切片扩容
首先我们来了解一个知识点:
无论数组还是切片,都有长度限制。也就是追加切片的时候,如果元素正好在切片的容量范围内,直接在尾部追加一个元素即可。如果超出了最大容量,再追加元素就需要针对底层的数组进行复制和扩容操作了。
也就是说:
使用append方法给slice追加元素的时候,由于slice的容量还未满,因此等同于扩展了slice指向数组的内容,可以理解为重新切了一个数组内容附给slice,同时修改了数组的内容。
使用append方法给slice追加元素的时候,如果此时slice的容量已满,再进行追加时,超出了切片的容量,数组就会越界了,于是就会出现扩容操作
当需要扩容时,append会做哪些操作呢?
创建一个新的临时切片t,t的长度和slice切片的长度一样,但是t的容量是slice切片的2倍,新建切片的时候,底层也创建了一个匿名的数组,数组的长度和切片容量一样。
复制slice里面的元素到t里,即填入匿名数组中。然后把t赋值给slice,现在slice的指向了底层的匿名数组。
转变成小于容量的append方法。
举个例子,数组arr = [3]int{0, 11, 22},生成一个切片slice := arr[1:3],使用append方法往切片slice中追加元素33,将发生以下操作:
再回到刚刚举的例子,之所外部的实参切片变量slice不受形参切片变量s修改的影响,因为在执行完 s = append(s, 4) 这段代码后,切片s的指针指向的数组发生了扩容,其指针指向了新的数组,因此当再次修改其第二个元素的值时,是不会影响外部切片变量slice的。
切片容量计算
具体的切片容量计算可参考下面的例子:
数组 [0, 1, 2, 3, 4] 中,数组有5个元素。如果切片 s = [1, 2, 3],那么3在数组的索引为3,也就是数组还剩最后一个元素的大小,加上s已经有3个元素,因此最后s的容量为 1 + 3 = 4。如果切片是 s1 = [4],4的索引在数组中是最大的了,数组空余的元素为0,那么s1的容量为 0 + 1 = 1。具体如下表:
作业
好了,写到这里本次的分享就要接近尾声了,最后想想还是有必要留个作业给大家,检验一下大家对以上知识点的接受程度:
大家在评论中写下你的答案吧~
转载:
https://juejin.im/post/6888117219213967368