Golang是值传递还是引用传递

2022年7月12日 308点热度 0人点赞 0条评论

背景

先说结论,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 的值表现和指针一样:它们是对内部映射或者切片数据的指针的描述符。复制映射或者切片的值,不会复制它们指向的数据。复制接口的值,会产生一个接口值存储的数据的副本。如果接口值存储的是一个结构体,复制接口值将产生一个结构体的副本。如果接口值存储的是指针,复制接口值会产生一个指针的副本,而不是指针指向的数据的副本。

值传递

这里列出典型的值传递的例子

func main() {    i := 1    str := "old"
stu := student{name: "ada", age: 1}
modify(i, str, stu) fmt.Println(i, str, stu.age) //1 old 1}
func modify(i int, str string, stu student) { i = 5 str = "new" stu.age = 10}

可以发现,在函数里面修改了值之后,不会影响函数外的变量的值。

我们想要内部修改能影响到函数外的变量的值,怎么办呢?

答案是:传指针

因为传指针的值传递,复制的是指针本身,但是指针指向的地址是一样的。所以我们在函数内部的修改,能影响到函数外的变量的值。

func main() {    i := 1    str := "old"
stu := &student{name: "ada", age: 1}
modify(&i, &str, stu) fmt.Println(i, str, stu.age) //5 new 10}
func modify(i *int, str *string, stu *student) { *i = 5 *str = "new" stu.age = 10}

注意这可不是引用传递,只是因为我们传入的是指针,指针本身是一份拷贝,但是对这个指针解引用之后,也就是指针所指向的具体地址,是不变的,所以函数内部的修改,在函数外面是知道的。

map

了解清楚了传值和传引用,但是对于Map类型来说,可能觉得还是迷惑,一来我们可以通过函数修改它的内容,二来它没有明显的指针。

func main() {    users := make(map[int]string)    users[1] = "user1"
fmt.Printf("before modify: user:%v\n", users[1]) // before modify: user:user1 modify(users) fmt.Printf("after modify: user:%v\n", users[1]) // after modify: user:user2}
func modify(u map[int]string) { u[1] = "user2"}

我们都知道,值传递是一份拷贝,里面的修改并不影响外面实参的值,那为什么map在函数内部的修改可以影响外部呢?

通过查看源码我们可以看到,实际上make底层调用的是makemap函数,主要做的工作就是初始化hmap结构体的各种字段

func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap {    //...}

通过查看src/runtime/hashmap.go源代码发现,make函数返回的是一个hmap类型的指针*hmap。也就是说map===*hmap。现在看func modify(p map)这样的函数,其实就等于func modify(p *hmap),相当于传递了一个指针进来。

而对于指针类型的参数来说,只是复制了指针本身,指针所指向的地址还是之前的地址。所以对map的修改是可以影响到函数外部的。

chan类型

chan类型本质上和map类型是一样的,这里不做过多的介绍,参考下源代码:

func makechan(t *chantype, size int64) *hchan {    //...}

chan也是一个引用类型,和map相差无几,make返回的是一个*hchan

slice类型

而map和chan使用make函数返回的实际上是 *hmap*hchan指针类型,也就是指针传递。

slice虽然也是引用类型,但是它又有点不一样。

简单来说就是,slice本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice在传递时,形参是拷贝的实参这个slice,但他们底层指向的数组是一样的,拷贝slice时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组。

我们先看一个简单的例子,对slice的某一元素进行赋值。

type slice struct {    array unsafe.Pointer    len   int    cap   int}

下面举个例子:

func main() {    arr := make([]int, 0)    arr = append(arr, 1, 2, 3)    fmt.Printf("outer1: %p, %p\n", &arr, &arr[0])    modify(arr)    fmt.Println(arr)  // 10, 2, 3}
func modify(arr []int) { fmt.Printf("inner1: %p, %p\n", &arr, &arr[0]) arr[0] = 10 fmt.Printf("inner2: %p, %p\n", &arr, &arr[0])}
//输出://outer1: 0x14000112018, 0x14000134000//inner1: 0x14000112030, 0x14000134000//inner2: 0x14000112030, 0x14000134000//[10 2 3]

因为slice是引用类型,指向的是同一个数组。

可以看到,在函数内外,arr本身的地址&arr变了,但是两个指针指向的底层数据,也就是&arr[0]数组首元素的地址是不变的。

所以在函数内部的修改可以影响到函数外部,这个很容易理解。

再来看另外一个稍微复杂的例子,函数内部使用append。这个会稍微不一样。

func main() {    arr := make([]int, 0)    //arr := make([]int, 0, 5)    arr = append(arr, 1, 2, 3)    fmt.Printf("outer1: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr))    //modify(arr)    appendSlice(arr)    fmt.Printf("outer2: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr))    fmt.Println(arr)}
func appendSlice(arr []int) { fmt.Printf("inner1: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr)) //modify(arr) arr = append(arr, 1) fmt.Printf("inner2: %p, %p, len:%d, capacity:%d\n", &arr, &arr[0], len(arr), cap(arr)) //modify(arr) //&arr[0]的地址是否相等,取决于初始化slice的时候的capacity是否足够}

这个问题就相对复杂的多了。

分两种情况:

make slice的时候没有分配足够的capacity

arr := make([]int, 0) 像这种写法,那么输出就是:

outer1: 0x14000114018, 0x1400012e000, len:3, capacity:3inner1: 0x14000114030, 0x1400012e000, len:3, capacity:3inner2: 0x14000114030, 0x1400012c060, len:4, capacity:6outer2: 0x14000114018, 0x1400012e000, len:3, capacity:3[1 2 3]

Image

1.outer1: 外部传入一个slice,引用类型,值传递。2.inner1: 由于是值传递,所以arr的地址&arr变了,但是两个arr指向的底层数组首元素&arr[0],也就是array unsafe.Pointer3.inner2: 在内部调用append后,由于cap容量不够,所以扩容,cap=cap*2,重新在新的地址空间分配底层数组,所以数组首元素的地址改变了。4.回到函数外部,外部的slice指向的底层数组为原数组,内部的修改不影响原数组。

make slice的时候分配足够的capacity

arr := make([]int, 0, 5)

像这种写法,那么输出就是:

outer1: 0x1400000c030, 0x1400001c050, len:3, capacity:5inner1: 0x1400000c048, 0x1400001c050, len:3, capacity:5inner2: 0x1400000c048, 0x1400001c050, len:4, capacity:5outer2: 0x1400000c030, 0x1400001c050, len:3, capacity:5[1 2 3]

虽然函数内部append的结果同样不影响外部的输出,但是原理却不一样。

Image

不同点:

3.在内部调用append的时候,由于cap 容量足够,所以不需要扩容,在原地址空间增加一个元素,底层数组的首元素地址相同。4.回到函数外部,打印出来还是[1 2 3],是因为外层的len是3,所以只能打印3个元素,实际上第四个元素的地址上已经有数据了。只不过因为len为3,所以我们无法看到第四个元素。

那正确的append应该是怎么样的呢:

appendSlice(&arr)
func appendSlice(arr *[]int) { *arr = append(*arr, 1)}

传指针进去,这样拷贝的就是这个指针,指针指向的对象,也就是slice本身,是不变的,我们使用*arr可以对slice进行操作。

总结

Go里面没有引用传递,Go语言是值传递如果需要函数内部的修改能影响到函数外部,那么就传指针。map/channel本身就是指针,是引用类型,所以直接传map和channel本身就可以。slice的赋值操作其实是针对slice结构体内部的指针进行操作,也是指针,可以直接传slice本身。slice的append操作同时需要修改结构体的len/cap,类似于struct,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)

<全文完>

↓↓↓欢迎关注我的公众号:码农在新加坡↓↓↓

52260Golang是值传递还是引用传递

这个人很懒,什么都没留下

文章评论