最近在研究 CoreDNS 插件,但个人之前又没有 Go 语言基础,于是一边学习一边编写插件。可以说只用了 20% 的时间就摸清楚了 Go 语言 80% 的内容。但剩下的 20% 还没掌握,或者说是深刻理解。这不,遇到了个 for slice 的问题,于是记录下相关的解决办法和原理。
0x01 问题背景
我的CoreDNS插件需要实现从服务器拉取一系列数据,编码方式使用JSON。其中有数据是以JSON数组的方式返回的,但是在运行单元测试时,却出现了与预期不同的情况,所有的列表数据居然都变成了列表末尾的!我在 Go Playground 简单复现了这个问题(https://go.dev/play/p/nVtJGbByoDC?v=goprev):
代码:
package main
import "fmt"
type Top struct {
Name string
Nest Nested
}
type Nested struct {
SubName string
}
func main() {
top := []Top{
{"A", Nested{"AA"}},
{"B", Nested{"BB"}},
{"C", Nested{"CC"}},
}
copy1 := make([]*Top, 0)
copy2 := make([]*Top, 0)
for i, obj := range top {
fmt.Printf("%p\n", &obj)
fmt.Printf("%p\n", &(obj.Nest))
fmt.Printf("%p\n", &(top[i].Nest))
fmt.Printf("TopName: %s, SubName: %s\n", obj.Name, obj.Nest.SubName)
copy1 = append(copy1, &obj)
copy2 = append(copy2, &top[i])
}
fmt.Println(copy1[0])
fmt.Println(copy1[1])
fmt.Println(copy1[2])
fmt.Println(copy2[0])
fmt.Println(copy2[1])
fmt.Println(copy2[2])
}
选择 Go 1.21 版本运行,结果如下:
0xc00009a000
0xc00009a010
0xc000094130
TopName: A, SubName: AA
0xc00009a000
0xc00009a010
0xc000094150
TopName: B, SubName: BB
0xc00009a000
0xc00009a010
0xc000094170
TopName: C, SubName: CC
&{C {CC}}
&{C {CC}}
&{C {CC}}
&{A {AA}}
&{B {BB}}
&{C {CC}}
Program exited.
通过观察,其造成的原因显而易见:每次迭代时,实际上迭代变量 obj
都共享了同样的指针。所以每次复制到 copy1
时,复制的都是相同的指针值。所以每次迭代,指针指向的值被覆盖,导致整个数组都是最后的值。
0x02 解决方法
最简单的解决方法如下,只需在 for 中添加一行代码: obj := obj
for i, obj := range top {
fmt.Printf("%p\n", &obj)
obj := obj
fmt.Printf("%p\n", &obj)
fmt.Printf("%p\n", &(obj.Nest))
fmt.Printf("%p\n", &(top[i].Nest))
fmt.Printf("TopName: %s, SubName: %s\n", obj.Name, obj.Nest.SubName)
copy1 = append(copy1, &obj)
copy2 = append(copy2, &top[i])
}
为了方便对比,加了一行打印 obj
的语句。再次运行结果如下:
0xc000090020
0xc000090040
0xc000090050
0xc000092190
TopName: A, SubName: AA
0xc000090020
0xc000090060
0xc000090070
0xc0000921b0
TopName: B, SubName: BB
0xc000090020
0xc000090080
0xc000090090
0xc0000921d0
TopName: C, SubName: CC
&{A {AA}}
&{B {BB}}
&{C {CC}}
&{A {AA}}
&{B {BB}}
&{C {CC}}
Program exited.
0x03 原理
在执行 obj := obj
时,就是进行了一次浅复制,将 slice
中每次迭代共享的内容复制了一份,保存到新变量 obj
中。新变量 obj
有自己的内存地址,于是就不受 slice
继续迭代的影响了。
附:Go 1.22 的表现
最开头的代码,选择 Go 1.22 版本运行,结果如下:
0xc000068020
0xc000068030
0xc00006a190
TopName: A, SubName: AA
0xc000068040
0xc000068050
0xc00006a1b0
TopName: B, SubName: BB
0xc000068060
0xc000068070
0xc00006a1d0
TopName: C, SubName: CC
&{A {AA}}
&{B {BB}}
&{C {CC}}
&{A {AA}}
&{B {BB}}
&{C {CC}}
Program exited.
可见最新版已经没有这个问题了,每次迭代都会为迭代变量开辟新的内存空间。
参考文档
[1] Use Pointer of for Range Loop Variable in Go
https://medium.com/swlh/use-pointer-of-for-range-loop-variable-in-go-3d3481f7ffc9