BUG日记:Go的切片引用传递?

BUG日记:Go的切片引用传递?

今天在写代码中遇到了一个问题,当我想使用函数来向切片中追加元素时发现无效。

需求为:遍历一棵路由树,将匹配节点的处理函数添加进一个slice里,最终返回,因为需要递归,所以想的传一个切片的参数,每匹配一个的时候就加入这个列表

最后由上级的函数来同一返回这个列表。

在我印象中,切片是引用传递,数组是值传递,按道理来说,只要对传进来的这个切片进行追加,最终所有结果都应该在这里面。但后面发现,递归函数没出问题,但递归结束后,切片为空,一个元素也没有。

我查了下,切片确实是引用传递,奇怪了,于是开始自己测试。

如以下代码:

func main()  {
	s := []string{}
	s = append(s, "1", "2", "3")
	change(s)
    fmt.Println(s)
}

func change(s []string)  {
	s[0] = "4"
}

由于切片是引用传递,所以结果应该是[4 2 3],程序跑出来确实如此,证明确实是引用传递,切片s中的值确实被改变了

我又换了改变s的方式,向s中追加元素,将change函数更改为

func change(s []string)  {
	s = append(s, "4")
}

但结果没有任何变化,这个4没有成功的被追加进s中,在这里,切片是引用传递的的话,就不应该没变化

于是我想到了看下s的地址

func main()  {
	s := []string{}
    fmt.Printf("%p : %v\n", s, s)
	s = append(s, "1", "2", "3")
	change(s)
    fmt.Printf("%p : %v\n", s, s)
}

func change(s []string)  {
	s = append(s, "4")
    fmt.Printf("%p : %v\n", s, s)
}

结果如下

0xb572f8 : []
0xc00004e060 : [1 2 3 4]
0xc000078480 : [1 2 3]

第一行为空切片时的地址,第二行为在change函数里查看的s的地址和值,最后一行为执行了change函数后s的地址和值

可以看出,在执行append后,引用的地址变了,我查了下资料,发现这个很好理解。

Slice切片底层结构如下

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

len为当前切片的长度,cap为整个切片的容量,当len == cap时在此进行append追加,切片会进行自动扩容,切片也重新指向了另外一个相关数组

切片扩容策略:

  • 如果期望容量大于当前容量的两倍就会使用期望容量
  • 如果当前切片的长度小于 1024 就会将容量翻倍
  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量

照这样的思路:

  1. 初始化时,切片s的len和cap都为0
  2. 第一次append,添加了三个元素,容量不足,需要扩容,期望容量为3 > 2*0,所以按照期望容量扩容,append后len和cap都为3
  3. 第二次append,添加了一个元素,容量再次不足,需要扩容,期望容量为4 , 4 < 3*2=6,按照两倍扩容,append后len为4,cap为6

可以验证以下,结果确实如此。

既然是扩容引起的指针变化,那我们是不是把初始容量设大一点就应该没问题了?

func main()  {
	s := make([]string, 0, 10)
    fmt.Printf("%p : %v\n", s, s)
	s = append(s, "1", "2", "3")
	change(s)
    fmt.Printf("%p : %v\n", s, s)
}

func change(s []string)  {
	s = append(s, "4")
    fmt.Printf("%p : %v\n", s, s)
}

如上,我新建了一个容量为10的切片,运行结果如下

0xc0000a2140 : []
0xc0000a2140 : [1 2 3 4]
0xc0000a2140 : [1 2 3]

可以看到,确实,在append过后,地址没变,但奇怪的是,尽管地址始终没变,但元素4还是没添加得进去

通过查询资料,在文章Go 切片绕坑指南 | Go 技术论坛 (learnku.com)中找到了类似的问题

通过这篇文章的解答,我有了一点自己的想法。

切片传值确实是传的引用,但传的不是真正的引用。

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

我认为传的是slice里面的,array这个数组的地址,这样也就可以解释。为什么我们能在另一个函数中通过赋值来改变原切片中的值,但不能通过append来为原切片追加元素,因为只传了array进来,len和cap是不变的,我们在末尾追加了元素对外界来说,也是不可见的。因为对于切片slice来说,只有前len个元素是有效的。

怎么验证我这种想法呢?我想的是,既然函数传参,Go只给你传进来了array,我就想办法把整个slice都传进来。解决办法就是:指针。把s取指传给change函数,函数来对这个指针进行操作。代码如下:

func main()  {
	s := make([]string, 0, 10)
    fmt.Printf("%p : %v\n", s, s)
	s = append(s, "1", "2", "3")
	change(&s)
    fmt.Printf("%p : %v\n", s, s)
}

func change(s *[]string)  {
	*s = append(*s, "4")
    fmt.Printf("%p : %v\n", *s, *s)
}

结果如我设想的一样,这样就是对的了,可以看到,4成功添加了进去。但我不太确定这样的做法是否正确或者说是合理,只是目前通过这样来解决了我的问题

0xc0000640a0 : []
0xc0000640a0 : [1 2 3 4]
0xc0000640a0 : [1 2 3 4]

未完待续...

以上是我目前知识范围能想到的最合理的解释,并不一定正确,后续会在进一步的学习验证后来补充这篇文章

上一篇:Oracle增量备份和快速备份(块改变跟踪Block Change Tracking)


下一篇:linux常用命令的英文单词缩写