发现问题
首先说明go中只有值传递,这里的引用传递只是一个相对概念。
问题起源于新生群的讨论,大家说到 go
时有人问 slice
是不是指针传递,我说是,有位学长说他曾经写过一段和 slice
有关的代码,很怪,怀疑不是引用传递。故事就这样开始了。
代码如下:
func main() {
sliceA := []int{1, 2, 3, 4, 5}
fmt.Println(sliceA)
changeSlice(sliceA)
fmt.Println(sliceA)
}
func changeSlice(slicePass []int) {
slicePass = slicePass[0:3]
}
// Output
/*
[1 2 3 4 5]
[1 2 3 4 5]
*/
看完这串代码我也惊了,在我曾经的想法里,slice
是会变的,但在这里却还是原样输出。我怀疑是 slice
的问题,于是上网搜寻信息,原来已经有人碰到同样的坑了。
过程有点艰难,看到一些说是 slice
扩容新指针导致的,又有一些说是 len
未被改变导致的,于是研究了一会儿才整理起来。
一些思考
main
中的 slice
没变化有两种情况
cap
够,slice
没扩容
slice
只是引用类型而非“完美”的引用传递。slice
本身是一个结构体
// runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
底层数组的确用指针表示,但是 slice
中的 len
与 cap
却是 int
类型。
也就是说,len
和 cap
是值传递而底层数组是引用传递,这就导致函数内改变了底层数组却没有改变 len
。所以 len
仍为5,那么在 main
的输出中依旧输出了5个,且由于数组内存连续,输出的第五个数就是原来的5,也就是说输出了已经不属于 slice
的第五个数。
例如下面:
func main() {
sliceA := []int{1, 2, 3, 4, 5}
fmt.Println(sliceA)
fmt.Printf("%d %p pass\n", len(sliceA), sliceA)
changeSlice(sliceA)
fmt.Println(sliceA)
fmt.Printf("%d %p main\n", len(sliceA), sliceA)
}
func changeSlice(slicePass []int) {
slicePass = slicePass[2:3]
slicePass[0] = 100
}
//Output
/*
[1 2 3 4 5]
5 0xc00000c690 pass
[1 2 100 4 5]
5 0xc00000c690 main
*/
截取的操作没有改变底层数组,且不需要扩容。所以可以看到切片中的值是被改变了,但是截取后的slicePass
的指针header
没有变, main
中的 len
和cap
也没有变。 最终读取的内存范围没有变化,依旧把之前的 1
2
4
5
都读出来了。
再看下面这个例子:
func main() {
sliceA := make([]int, 3, 4)
sliceA[0] = 0
sliceA[1] = 1
sliceA[2] = 2
fmt.Println(sliceA)
changeSlice(sliceA)
fmt.Println(sliceA)
fmt.Println(sliceA[:4]) //强制输出第四项
}
func changeSlice(slicePass []int) {
slicePass = append(slicePass, 3)
}
//Output
/*
[0 1 2]
[0 1 2]
[0 1 2 3]
*/
我们指定了一个 len
为3,cap
为4的 slice
。append
完后发现正常输出只会输出前三个数,这就验证了 len
并没有被改变。而当我们强制输出第四项时又发现3是存在的。
也就是说这种情况下 append
对原数组生效,只是由于 len
没有改变而无法呈现出 append
的项。
cap
不够, slice
扩容
func main() {
sliceA := []int{1, 2, 3, 4, 5}
fmt.Println(sliceA)
fmt.Printf("%d %p main\n", len(sliceA),sliceA)
changeSliceA(sliceA)
fmt.Println(sliceA)
}
func changeSliceA(slicePass []int) {
slicePass = append(slicePass, 6)
fmt.Printf("%d %p pass\n", len(slicePass),slicePass)
}
// Output
/*
[1 2 3 4 5]
5 0xc00000c690 main
6 0xc000016550 pass
[1 2 3 4 5]
*/
这个例子中我们还是 append
了一个6,但这个例子和上面不同。这个例子中的 cap
= len
,当我们 append
时会造成 slice
扩容。
当发生
growslice
时,会给slice
重新分配一段更大的内存,然后把原来的数据copy
过去,把slice
的array
指针指向新内存。
所以 slicePass
的指针已经和原来不同,那么这个 append
的新数自然无法在原数组中呈现,而且在原数组中不存在。
回到问题
再看学长的代码,可以看出这属于第一种情况, slice
没有扩容,前后指针相同。而且这个片段非常巧合,函数中只是做个了截取的动作而没有改变任何值,本质来讲数据没变动。那么由于len
没改变,输出也就和之前一模一样了,造成了 slice
是值传递的假象(当然 go
中本质上只有值传递)。
解决方案
其实也很简单,为了 len
和 cap
都能变化,我们将切片的指针传入,那么所有属性也就跟着变化了。
func main() {
sliceA := []int{1, 2, 3, 4, 5}
fmt.Println(sliceA)
changeSlice(&sliceA)
fmt.Println(sliceA)
fmt.Printf("%d main\n", len(sliceA))
}
func changeSlice(slicePass *[]int) {
*slicePass = append(*slicePass, 6)
fmt.Printf("%d pass\n", len(*slicePass))
}
//Output
/*
[1 2 3 4 5]
6 pass
[1 2 3 4 5 6]
6 main
*/
可以看到 len
被改变了,那么6加入了且也能正常被读取输出了。
总结
go 中的 slice
在函数中被 append
时数据呈现不变分为两种情况。
一种是 len
未被改变,由传值导致;
一种是指针发生改变,由 slice
的内部扩容实现导致。
参考
- https://www.v2ex.com/t/662118
- https://www.jb51.net/article/136199.htm
- https://segmentfault.com/a/1190000006056800
- https://www.v2ex.com/t/496496
- https://www.v2ex.com/t/649712
- https://www.v2ex.com/t/496496
- https://www.v2ex.com/t/505352
- https://www.v2ex.com/t/457592
- https://www.golangtc.com/t/5565344ab09ecc3d42000026
技术文章,学习了。 夏天快乐,感谢博主的分享,支持了。