背景

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

值传递

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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
}

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

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

答案是:传指针

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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类型来说,可能觉得还是迷惑,一来我们可以通过函数修改它的内容,二来它没有明显的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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结构体的各种字段

1
2
3
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类型是一样的,这里不做过多的介绍,参考下源代码:

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

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

slice类型

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

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

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

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

1
2
3
4
5
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

下面举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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。这个会稍微不一样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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]

slice

  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的结果同样不影响外部的输出,但是原理却不一样。

slice

不同点:

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

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

1
2
3
4
5
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,如果需要传递到函数外部,需要传指针。(或者使用函数返回值)

<全文完>