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

#值传递

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

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: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]

  1. outer1: 外部传入一个slice,引用类型,值传递。
  2. inner1: 由于是值传递,所以 arr 的地址&arr变了,但是两个 arr 指向的底层数组首元素&arr[0],也就是array unsafe.Pointer
  3. inner2: 在内部调用append后,由于cap容量不够,所以扩容,cap=cap*2,重新在新的地址空间分配底层数组,所以数组首元素的地址改变了。
  4. 回到函数外部,外部的 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的结果同样不影响外部的输出,但是原理却不一样。

不同点:

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

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

appendSlice(&arr)

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

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

原文地址 zhuanlan.zhihu.com

#总结

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