Review Slice Internal When Solving A Golang Puzzle
Preface
在一次日常编程练习中,碰到一个开始觉得颇为奇怪的Golang behaviour,在解决问题过程中,弄清楚了两个Golang比较细节的feature,很有趣的experience,分享给大家。
Peculiar Issue
下面代码,首先将array作为map的key,遍历map的key,将array插入到二维slice中,见playground:
1 | mp := make(map[[3]int]bool) |
运行出乎意料,看来后续的append将之前的全部覆盖了,这是怎么回事?
1 | key is [1 2 3] |
Asking a problem
百思不得其解,于是在go-nuts上提问了一发,很快得到回答,不得不说上面真是高手云集,氛围也相当好[^1]。
Got a answer
输出涉及到Golang的两个feature: loop variable reuse及slice internal,我们逐一分析
Loop variable reuse
我们loop over map key的代码其实可以展开成为,见playground:
1 | a1 := [3]int{1, 2, 3} |
同样能重现出被后续append覆盖的现象。我们注意被append到[][]int的a被reuse,如果换成每次append新[3]int问题就不会出现,见playground
1 | s := make([][]int, 0) |
可见是variable reuse引发的问题,but wait:
1 | for k := range mp { |
这个for怎么看着都像每次新建variable,不是吗?
还真的不是,参见这个issue,有位高手是这么回复的:
It is, unfortunately, a very common “gotcha”.
And wtf is a Gotcha? wikipedia说道:
Gotcha,在计算机编程领域指在系统、或程序、程序设计语言中一个合法有效的构造但是反直觉、总是容易造成错误,易于使用但其结果是不期望的或者不合逻辑的。 字面上是got you的简写,常用于口语,直译为: “逮着你了”、“捉弄到你了”、“你中计了” 、“骗到你了”
社区已经有人提议改成每次都新建variable而不是reuse了[^2]
但为什么reuse了一个slice,就会造成覆盖的结果呢?这个涉及到slice的内部结构。
Slice internals
我们看下Slice的内部结构[^3]:
我们append时:
1 | s := make([][]int, 0) |
其实是将slice的header struct的指针append到二维数组,这里[][]int非常像c中的指针数组,由于我们复用了slice struct的指针,我们append到[][]int的其实是同样的slice struct的指针,同时slice struct包含指向同一片数据的指针,这就是为什么会出现这样的输出:
1 | after append: [[7 8 9] [7 8 9] [7 8 9]] |
示意图:
[^1]: Append array to slice of slice behavior puzzles me
[^2]: proposal: spec: redefine range loop variables in each iteration
[^3]: Go Slices: usage and internals