Golang中关于func变量的Bug(已在1.20版本修复?)

Golang中关于func变量的Bug(已在1.20版本修复?) 我发现了一个错误,该错误似乎从 1.20 版本开始已被修复。然而,发布说明中似乎没有提到任何类似的内容,所以如果有人能明确指出它,我会感觉更放心。

我已将其简化为以下测试。这个测试在 1.19 版本中会失败。但是,取消函数 test() 第一行的注释(该行仅仅是引入了两个空白变量)会使测试通过(然而,只引入一个空白变量是不够的)。正如我所说,无论该行是否被注释掉,测试在 1.20 版本中都会通过。那么发生了什么变化?

谢谢!

4 回复

感谢!内联似乎确实是问题的根源,对吧?我已经考虑过在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跟踪器中找到相关讨论,特别是与循环变量和闭包捕获相关的问题。

回到顶部