slice是完美的引用传递吗
发现问题
首先说明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)
fmt.Printf("%d %p main\n", len(sliceA),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]
5 0xc00000c690 main
*/
这个例子中我们还是 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 的内部扩容实现导致。
参考
转载说明
- 博主: iyear
- 发布时间:2021 年 07 月 16 日
- 字数 3879
- 原始链接:slice是完美的引用传递吗