又名 “go slice作为函数参数是 “引用传递” 勘误 ”
1、问题描述
在很多Go的文章、博客中都有提到这样的内容:slice 在作为函数入参时采用的是引用(地址)传递。并且这几乎成了 “众所周知” 的知识点。
平时并没有留意别人是怎么看这里的,但最近面试和工作中发现上述观点几乎得到了其他人的共同认可。
不过在我初学Go的时候有幸看到过另一种说法,最近重新查阅资料整理代码给大家分享一下。
以下内容如果读者有什么不同见解和分析,欢迎留言或者发送邮件讨论交流。
2、证明分析
以下采用反证法
先假设 “slice 在作为函数入参时采用的是引用(地址)传递” 成立,那么根据引用的特征可以知道:函数外的slice(后续简写为 slice外 ) 和 函数内的slice(后续简写为 slice内)行为应当完全一致。
slice底层结构:
type slice struct {
array unsafe.Pointer
len int
cap int
}
那么与之对应的应当有一下特征:
slice.array
在 slice内 改动后 slice外 也会发生对应的改变slice.len
在 slice内 改动后 slice外 也会发生对应的改变slice.cap
在 slice内 改动后 slice外 也会发生对应的改变
上述3个特征是必要条件,只要有一个不满足,则结论不成立。
2.1、一段代码示例
package main
import "fmt"
func main() {
si := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
fmt.Printf("%v len %d\n", si, len(si))
// fmt.Printf("1 %v len %d cap %d %p\n", si, len(si), cap(si), &si)
test1(si)
fmt.Printf("%v len %d\n", si, len(si))
// fmt.Printf("2 %v len %d cap %d %p\n", si, len(si), cap(si), &si)
}
func test1(si []int) {
// fmt.Printf("3 %v len %d cap %d %p\n", si, len(si), cap(si), &si)
si = append(si[:3], si[4:]...)
// fmt.Printf("4 %v len %d cap %d %p\n", si, len(si), cap(si), &si)
}
该代码的运行结果如下:
[1 2 3 4 5 6 7 8 9] len 9
[1 2 3 5 6 7 8 9 9] len 9
2.2、推论结果
从代码示例及运行结果可以看出:
- 函数内部对 slice结构中数组(
slice.array
)的操作,对于函数外部来说部分成功了(第4个元素已经成功删除) - 函数内部对 slice 结构中长度(
slice.len
)的操作,对于函数外部来说失败了
第二次预期的输出是输出长度8、元素8个,但实际第二次输出长度是9,输出元素是9个
2.2.1、分析及结论:
实践后的结果与预期不符,假设成立的必要条件无法满足,也就是说:slice做为参数是引用(或地址)传递 不成立。
即:slice作为参数是值传递 成立。
2.2.2、进一步解答:
-
问题一:既然是值传递,那为什么数组内容变了?
因为slice整个结构是值传递,但slice中的数组是个 指针,在进行数据复制的过程中新的slice的数组内容使用的还是 原指针的值,因此函数内slice和外部的slice使用的是 同一份数据。
所以函数内部对 slice 数组的操作也会反馈到函数外的 slice上。
但是长度、容量这两个是整型数值,不是指针类型,因此复制的过程中只是复制的内容,在函数内部的操作也不会影响外部。
参考资料及附录
- go语言切片作为函数参数的研究 苏幕遮_凌枫 2019-02-01 23:49
- slice(一)——初识底层结构 LeslieRan 2019-12-11 16:33
- 关联的相悖知识:secguide/Go安全指南.md
本文由 qingchuwudi 译制或原创,除非另有声明,在不与原著版权冲突的前提下,本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。