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% 的容量,直到新容量大于期望容量
照这样的思路:
- 初始化时,切片s的len和cap都为0
- 第一次append,添加了三个元素,容量不足,需要扩容,期望容量为3 > 2*0,所以按照期望容量扩容,append后len和cap都为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]
未完待续...
以上是我目前知识范围能想到的最合理的解释,并不一定正确,后续会在进一步的学习验证后来补充这篇文章