Golang中关于func变量的Bug(已在1.20版本修复?)
Golang中关于func变量的Bug(已在1.20版本修复?) 我发现了一个错误,该错误似乎从 1.20 版本开始已被修复。然而,发布说明中似乎没有提到任何类似的内容,所以如果有人能明确指出它,我会感觉更放心。
我已将其简化为以下测试。这个测试在 1.19 版本中会失败。但是,取消函数 test() 第一行的注释(该行仅仅是引入了两个空白变量)会使测试通过(然而,只引入一个空白变量是不够的)。正如我所说,无论该行是否被注释掉,测试在 1.20 版本中都会通过。那么发生了什么变化?
谢谢!
感谢!内联似乎确实是问题的根源,对吧?我已经考虑过在Reddit上提问了;看来你帮我做了决定 🙂
更多关于Golang中关于func变量的Bug(已在1.20版本修复?)的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
供记录:cmd/compile: incorrect inlining of function swapping two funcs · Issue #59108 · golang/go
来自该问题的一条评论:
此错误在1.16版本中引入,1.15版本中两个函数都还存在,从1.16到1.19版本,内联的b函数被优化掉了。
该修复的回溯移植到1.19版本正在进行中。
感谢 @fabe-xx 推动此问题的进展。
这个问题真的非常有趣,简直要把我逼疯了!在 1.19 版本中,当你注释掉空结构体变量那一行时,test 函数被内联了,但编译器不知何故遗漏/忽略了 a, b = b, a 这个交换操作。我实在搞不明白这是怎么发生的!我尝试用 GOSSAFUNC=main 编译,但即使是最开始的 AST 也已经被重写为内联循环,而这竟然丢弃了第二个函数的主体!?
我强烈建议你去 Reddit 上提问。如果你不去,我就去!我真的很想弄明白这里到底发生了什么!
func main() {
fmt.Println("hello world")
}
根据你提供的测试代码分析,这是一个关于函数变量闭包捕获行为的边界情况。在Go 1.19及之前版本中确实存在一个编译器优化相关的bug,该bug在特定条件下会导致函数变量捕获错误的变量值。
问题分析
在你的测试代码中,关键问题出现在闭包捕获循环变量时的行为不一致性。当存在特定数量的未使用变量时,编译器生成的代码会有所不同。
func test() {
// 这行注释/取消注释会影响测试结果
_, _ = 1, 2 // 两个空白变量
var funcs []func() int
for i := 0; i < 3; i++ {
funcs = append(funcs, func() int { return i })
}
// 期望:0, 1, 2
// Go 1.19实际:3, 3, 3(没有空白变量时)
// Go 1.19实际:0, 1, 2(有两个空白变量时)
}
修复详情
这个bug在Go 1.20中通过CL 435256修复。具体来说,修复涉及编译器对循环变量在闭包中捕获方式的优化处理。
在Go 1.19及之前版本中:
- 编译器在某些优化场景下会错误地重用循环变量的存储位置
- 导致所有闭包都捕获到循环结束后的最终值(
i = 3) - 空白变量的存在改变了编译器的优化决策,从而绕过了这个bug
在Go 1.20中:
- 修复了循环变量在闭包上下文中的分配和捕获逻辑
- 确保每个闭包捕获的是循环迭代时的变量快照
- 无论是否有空白变量,行为都保持一致
验证修复
你可以通过以下代码验证修复:
package main
import "fmt"
func main() {
// 测试1:没有空白变量
var funcs1 []func() int
for i := 0; i < 3; i++ {
funcs1 = append(funcs1, func() int { return i })
}
// 测试2:有空白变量
_, _ = 1, 2
var funcs2 []func() int
for j := 0; j < 3; j++ {
funcs2 = append(funcs2, func() int { return j })
}
// Go 1.20+:两者都输出 0, 1, 2
fmt.Println("Without blank vars:", funcs1[0](), funcs1[1](), funcs1[2]())
fmt.Println("With blank vars:", funcs2[0](), funcs2[1](), funcs2[2]())
}
正确的闭包写法
虽然bug已修复,但最佳实践仍然是显式捕获循环变量:
func test() {
var funcs []func() int
for i := 0; i < 3; i++ {
i := i // 创建局部副本
funcs = append(funcs, func() int { return i })
}
// 现在无论哪个版本都保证输出 0, 1, 2
}
这个修复确实没有在Go 1.20的发布说明中单独列出,因为它被包含在更大的编译器优化修复集合中。你可以在Go项目的issue跟踪器中找到相关讨论,特别是与循环变量和闭包捕获相关的问题。

