起飞就起飞

golang中slice的引用类型的坑

Posted on By baixiao

先说结论:

  • golang中函数传参只有值传递,没有引用传递
  • golang中的slice/map/channel是引用类型

针对第一点,官方文档说的很清楚:

In a function call, the function value and arguments are evaluated in the usual order. After they are evaluated, the parameters of the call are passed by value to the function and the called function begins execution.

来看例子:

func main() {
	ids := make([]string, 0)
	fmt.Printf("&ids     addr:%p\n", &ids)
	ids = append(ids, "23")
	fmt.Printf("&ids[0]  addr:%p\n", &ids[0])
	
	funca := func(productId string, idsa []string) {
		fmt.Printf("&idsa    addr:%p\n", &idsa)
		fmt.Printf("&idsa[0] addr:%p\n", &idsa[0])
		idsa = append(idsa, productId)
		fmt.Printf("&idsa[0] addr:%p after append\n", &idsa[0])
		fmt.Printf("&idsa[1] addr:%p\n", &idsa[1])
	}
	
	productId := "144"
	funca(productId, ids)
	fmt.Println(ids)
	
	funcb := func(idsb []string) {
		for i := range idsb {
			idsb[i] = fmt.Sprint(i)
			fmt.Printf("&idsb[i] addr:%p\n", &idsb[i])
		}
	}
	
	funcb(ids)
	fmt.Println(ids)
}

看结果:

&ids     addr:0xc42000a060
&ids[0]  addr:0xc42000e1d0
&idsa    addr:0xc42000a080
&idsa[0] addr:0xc42000e1d0
&idsa[0] addr:0xc42000a0a0 after append
&idsa[1] addr:0xc42000a0b0
[23]
&idsb[i] addr:0xc42000e1d0
[0]

先不看地址相关打印,两个函数funcafuncb的目的分别是增加slice的元素和修改slice的值。可见,funca是无效的而funcb有效。为什么呢?

疑问有两点:

  1. 既然函数传参是值传递,为什么funcb可以修改实参ids的值?
  2. 既然funcb有效,为什么funca的append操作无效?

那么从地址打印来看,&ids&idsa的地址不同,所以证实了函数传参不是引用传递,确实是值传递。但是,&ids[0]&idsa[0]以及&idsb[i]的地址都是0xc42000e1d0,这是一个很奇怪的现象。

这时需要看看slice的结构了:

slice是一种数据结构,它描述的是与slice变量本身相隔离的,存储在数组里的连续部分。slice不是数组,slice描述的是一段数组。

在函数传参时,slice传递了一个含有指针和长度的结构的值,而不是一个指向结构的指针。这一点非常重要。

因此,实参ids和行参idsaidsb是指向的同一段数组。所以funcb是可以修改实参的值。第一个问题解决了。再看funca里面的append之后,idsa[0]的地址改变了,所以说append之后实际上idsa指向了另一个数组。第二个问题也解决了。

另外,一开始可能会奇怪,&ids的地址0xc42000a060&ids[0]的地址0xc42000e1d0在地址空间中差得很远,正说明了slice和slice指向的数组是两个东西,分配的是两块内存地址。只不过slice会指向关联数组,slice的结构中保存了数组的地址。

所以我们的面试题就很简单了:

type T struct {
	ls []int
	v  int
}
	
func foo(t T) {
	t.ls[0] = 999
	t.v = 888
}
	
func main() {
	var t = T{ls: []int{1, 2, 3}}
	foo(t)
	fmt.Println(t)
}