结论:Go 1.22 之前,for range 的循环变量每轮迭代复用同一个变量,闭包捕获的是变量本身而非当轮值——循环里起 goroutine 或存闭包,最终看到的都是最后一轮的值。Go 1.22 起改为每轮迭代创建新变量,经典陷阱被语言层面消除。
展开:1.22 前的标准翻车场景:
for _, v := range []int{1, 2, 3} {
go func() { fmt.Println(v) }() // 旧版本:可能全打印 3
}
老代码的规避写法有两种:循环体内 v := v 影子复制,或把值作为参数传入 go func(v int){...}(v)。升级注意点:1)该变更属于语言语义修改,go.mod 里 go 1.22 及以上才启用新语义,老模块保持旧行为(GOFLAGS=godebug 可强制);2)修复后性能几乎无感(变量本来就要拷贝),但依赖旧行为的怪异代码(刻意共享循环变量)会悄悄改变语义;3)静态检查工具(loopvarcapture、go vet 的 loopclosure)仍能帮老项目提前发现问题。易错点:陷阱只在闭包引用循环变量时出现,普通循环体内直接用值不受影响。
追问方向:闭包捕获变量是引用语义这一根源、Go 为何敢做不向后兼容的语义变更(Go 1 兼容性承诺的边界)。