这个更新非常重要,因为它直接终结了 Go 语言过去十年里最常见的 Bug 之一。
在 Go 1.22 之前,我们常说"不要在闭包里直接引用循环变量",但现在,这个规则变了。
1. 过去的"坑"(Go 1.21 及更早)
在旧版本里,for 循环中的变量 i 是地址复用的。每一轮循环,i 都是同一个变量,只是值在变。
func main() {
done := make(chan bool)
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v) // 闭包引用了同一个变量 v 的地址
done <- true
}()
}
// ... 等待输出
}
- 旧版本结果:大概率输出
c, c, c。因为协程启动慢,等它们运行时,变量v已经变成最后那个 "c" 了。 - 当时的解法:必须通过传参或者在循环内定义同名局部变量
v := v。
2. Go 1.22 的"史诗级"变化
从 Go 1.22 开始,官方修改了语义:在 for 循环中,每一轮迭代都会创建一个新的变量实例。
- 现在的代码(就是上面那段):直接输出
a, b, c(顺序随机)。 - 底层原理:编译器在每一轮循环里都为你自动做了一次变量隔离。每一轮的
v都是独立的,闭包捕获的是那个瞬间的"专属"变量。
3. 为什么说这比较重要?
- 心智负担降低:你再也不用为了防范"闭包陷阱"到处写
v := v这种看起来像废话的代码了。 - 安全性提升:这是 Go 团队为了解决新手甚至老手都常犯的错误,罕见地打破了"保持旧逻辑不变"的传统。
- 性能几乎无损:编译器优化得非常好,这种变量分配对绝大多数程序来说没有明显的性能开销。
⚠️ 一个小提醒
这个特性生效的前提是:你的 go.mod 文件里的版本号必须声明为 go 1.22 或更高。如果写的是 1.21,编译器还是会按旧的那套"有坑"的逻辑来执行。
Go
package main
import (
"fmt"
"sync"
"time"
)
func main() {
values := []string{"a", "b", "c"}
var wg sync.WaitGroup
fmt.Println("--- 开始执行 ---")
for _, v := range values {
wg.Add(1)
go func() {
defer wg.Done()
// 在 Go 1.22 之前,这里的 v 共享同一个地址,最后打印的通常都是最后一个元素
// 在 Go 1.22 之后,这里的 v 每轮都是新变量,能正确打印 a, b, c
fmt.Println("当前值:", v)
}()
}
wg.Wait()
fmt.Println("--- 执行结束 ---")
}
旧版的标准写法
Go
for _, v := range values {
wg.Add(1)
go func(val string) {
defer wg.Done()
fmt.Println(val)
}(v)
}