Golang是值传递还是引用传递
背景
先说结论,Go里面没有引用传递
,Go语言是值传递
。很多技术博客说Go语言有引用传递,都是没真的理解Go语言。
值传递
指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递
指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
而Go语言中的一些让你觉得它是引用传递
的原因,是因为Go语言有值类型
和引用类型
,但是它们都是值传递
。
值类型
- int、float、bool、string、array、sturct等
引用类型
-
slice,map,channel,interface,func等
-
引用类型作为参数时,称为浅拷贝,形参改变,实参数跟随变化.因为传递的是地址,形参和实参都指向同一块地址
-
值类型作为参数时,称为深拷贝,形参改变,实参不变,因为传递的是值的副本,形参会新开辟一块空间,与实参指向不同
-
如果希望值类型数据在修改形参时实参跟随变化,可以把参数设置为指针类型
如果对Go语言只有值传递
有不同想法的,请看官网的解释。
官网解释:https://go.dev/doc/faq#pass_by_value
When are function parameters passed by value?
As in all languages in the C family, everything in Go is
passed by value
. That is, a function always gets a copy of the thing being passed, as if there were an assignment statement assigning the value to the parameter. For instance, passing an int value to a function makes a copy of the int, and passing a pointer value makes a copy of the pointer, but not the data it points to. (See a later section for a discussion of how this affects method receivers.)
Map and slice values behave like pointers: they are descriptors that contain pointers to the underlying map or slice data. Copying a map or slice value doesn’t copy the data it points to. Copying an interface value makes a copy of the thing stored in the interface value. If the interface value holds a struct, copying the interface value makes a copy of the struct. If the interface value holds a pointer, copying the interface value makes a copy of the pointer, but again not the data it points to.
我来翻译一下:
像 C 家族中的其他所有语言一样,Go 语言中的所有传递都是传值。
也就是说,函数接收到的永远都是参数的一个副本,就好像有一条将值赋值给参数的赋值语句一样。
例如,传递一个 int 值给一个函数,函数收到的是这个 int 值得副本,传递指针值,获得的是指针值的副本,而不是指针指向的数据。
(请参考 [later section] (https://golang.org/doc/faq#methods_on_values_or_pointers) 来了解这种方式对方法接收者的影响)
Map 和 Slice 的值表现和指针一样:它们是对内部映射或者切片数据的指针的描述符。
复制映射或者切片的值,不会复制它们指向的数据。复制接口的值,会产生一个接口值存储的数据的副本。
如果接口值存储的是一个结构体,复制接口值将产生一个结构体的副本。
如果接口值存储的是指针,复制接口值会产生一个指针的副本,而不是指针指向的数据的副本。
值传递
这里列出典型的值传递的例子
可以发现,在函数里面修改了值之后,不会影响函数外的变量的值。
我们想要内部修改能影响到函数外的变量的值,怎么办呢?
答案是:传指针
。
因为传指针的值传递,复制的是指针本身,但是指针指向的地址是一样的。所以我们在函数内部的修改,能影响到函数外的变量的值。
注意这可不是引用传递
,只是因为我们传入的是指针,指针本身是一份拷贝,但是对这个指针解引用
之后,也就是指针所指向的具体地址,是不变的,所以函数内部的修改,在函数外面是知道的。
map
了解清楚了传值和传引用,但是对于Map类型来说,可能觉得还是迷惑,一来我们可以通过函数修改它的内容,二来它没有明显的指针。
|
|
我们都知道,值传递
是一份拷贝,里面的修改并不影响外面实参的值,那为什么map在函数内部的修改可以影响外部呢?
通过查看源码我们可以看到,实际上make
底层调用的是makemap
函数,主要做的工作就是初始化hmap
结构体的各种字段
通过查看src/runtime/hashmap.go
源代码发现,make
函数返回的是一个hmap
类型的指针*hmap
。也就是说map===*hmap
。 现在看func modify(p map)
这样的函数,其实就等于func modify(p *hmap)
,相当于传递了一个指针进来。
而对于指针类型的参数来说,只是复制了指针本身,指针所指向的地址还是之前的地址。所以对map的修改是可以影响到函数外部的。
chan类型
chan
类型本质上和map
类型是一样的,这里不做过多的介绍,参考下源代码:
chan
也是一个引用类型,和map
相差无几,make
返回的是一个*hchan
。
slice类型
而map和chan使用make函数返回的实际上是 *hmap
和*hchan
指针类型,也就是指针传递。
slice虽然也是引用类型,但是它又有点不一样。
简单来说就是,slice本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice在传递时,形参是拷贝的实参这个slice,但他们底层指向的数组是一样的,拷贝slice时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组。
我们先看一个简单的例子,对slice
的某一元素进行赋值。
下面举个例子:
|
|
因为slice
是引用类型,指向的是同一个数组。
可以看到,在函数内外,arr本身的地址&arr
变了,但是两个指针指向的底层数据,也就是&arr[0]
数组首元素的地址是不变的。
所以在函数内部的修改可以影响到函数外部,这个很容易理解。
再来看另外一个稍微复杂的例子,函数内部使用append
。这个会稍微不一样。
|
|
这个问题就相对复杂的多了。
分两种情况:
make slice的时候没有分配足够的capacity
arr := make([]int, 0)
像这种写法,那么输出就是:
outer1: 0x14000114018, 0x1400012e000, len:3, capacity:3
inner1: 0x14000114030, 0x1400012e000, len:3, capacity:3
inner2: 0x14000114030, 0x1400012c060, len:4, capacity:6
outer2: 0x14000114018, 0x1400012e000, len:3, capacity:3
[1 2 3]
- outer1: 外部传入一个
slice
,引用类型,值传递。 - inner1: 由于是值传递,所以arr的地址
&arr
变了,但是两个arr指向的底层数组首元素&arr[0]
,也就是array unsafe.Pointer
。 - inner2: 在内部调用
append
后,由于cap容量
不够,所以扩容,cap=cap*2
,重新在新的地址空间分配底层数组,所以数组首元素的地址改变了。 - 回到函数外部,外部的slice指向的底层数组为原数组,内部的修改不影响原数组。
make slice的时候分配足够的capacity
arr := make([]int, 0, 5)
像这种写法,那么输出就是:
outer1: 0x1400000c030, 0x1400001c050, len:3, capacity:5
inner1: 0x1400000c048, 0x1400001c050, len:3, capacity:5
inner2: 0x1400000c048, 0x1400001c050, len:4, capacity:5
outer2: 0x1400000c030, 0x1400001c050, len:3, capacity:5
[1 2 3]
虽然函数内部append
的结果同样不影响外部的输出,但是原理却不一样。
不同点:
- 在内部调用
append
的时候,由于cap 容量
足够,所以不需要扩容,在原地址空间增加一个元素,底层数组的首元素地址相同。 - 回到函数外部,打印出来还是
[1 2 3]
,是因为外层的len
是3,所以只能打印3个元素,实际上第四个元素的地址上已经有数据了。只不过因为len
为3,所以我们无法看到第四个元素。
那正确的append应该是怎么样的呢:
传指针进去,这样拷贝的就是这个指针,指针指向的对象,也就是slice本身,是不变的,我们使用*arr
可以对slice进行操作。
总结
- Go里面没有
引用传递
,Go语言是值传递
。 - 如果需要函数内部的修改能影响到函数外部,那么就传指针。
- map/channel本身就是指针,是引用类型,所以直接传map和channel本身就可以。
- slice的赋值操作其实是针对slice结构体内部的指针进行操作,也是指针,可以直接传slice本身。
- slice的append操作同时需要修改结构体的
len/cap
,类似于struct,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)
<全文完>