Go 在 for 循环中启动 goroutine 时,若匿名函数直接引用循环变量(如 i),所有 goroutine 会共享同一变量地址,最终打印出循环结束后的终值(如 6),根本原因在于闭包按引用捕获变量而非值。
go 在 for 循环中启动 goroutine 时,若匿名函数直接引用循环变量(如 `i`),所有 goroutine 会共享同一变量地址,最终打印出循环结束后的终值(如 `6`),根本原因在于闭包按引用捕获变量而非值。
在 Go 并发编程中,一个高频且隐蔽的陷阱是:在 for 循环中直接于匿名函数内使用循环变量。以下代码看似直观,实则暗藏风险:
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go func() {
fmt.Println(i) // ⚠️ 危险:所有 goroutine 共享同一个 i 变量
time.Sleep(time.Second)
wg.Done()
}()
}
wg.Wait()
}运行结果并非预期的 1 2 3 4 5(顺序可能不同),而是 6 6 6 6 6 ——即使 i 的合法取值范围是 1–5,最终却输出了 6。
? 为什么会这样?
关键在于 Go 闭包的变量捕获机制:
✅ Go 的匿名函数形成闭包时,捕获的是变量的内存地址(引用),而非创建瞬间的值;
✅ for i := 1; i <= 5; i++ 中的 i 是单个变量,在整个循环生命周期内复用其内存位置;
✅ 循环执行极快,而 goroutine 的调度存在延迟——当它们真正开始执行时,外层 for 循环早已结束,此时 i 已自增为 6(因 i <= 5 不再成立,循环退出);
✅ 所有 goroutine 中的 fmt.Println(i) 读取的,都是这个已稳定为 6 的同一内存地址。
? 这不是竞态(race)或 bug,而是 Go 语言设计使然:闭包绑定变量引用,以支持状态共享与延迟求值。问题根源在于未意识到“变量复用”与“执行延迟”的叠加效应。
✅ 正确解决方案(两种等效方式)
方案一:循环内声明同名局部变量(推荐,简洁清晰)
for i := 1; i <= 5; i++ {
i := i // ✅ 关键:遮蔽(shadowing)外层 i,创建独立副本
wg.Add(1)
go func() {
fmt.Println(i) // 此 i 是当前迭代专属值
time.Sleep(time.Second)
wg.Done()
}()
}方案二:将变量作为参数传入匿名函数(显式、语义明确)
for i := 1; i <= 5; i++ {
wg.Add(1)
go func(n int) { // n 是 i 的值拷贝(pass-by-value)
fmt.Println(n)
time.Sleep(time.Second)
wg.Done()
}(i) // ✅ 立即传入当前 i 值
}两种方案本质相同:切断闭包对外层循环变量的引用链,确保每个 goroutine 持有独立的值副本。
⚠️ 注意事项与最佳实践
- 不要依赖 runtime.GOMAXPROCS 或 time.Sleep “修复”此问题:它们无法改变变量捕获语义,仅掩盖症状;
- Go 1.22+ 对 for range 有优化:for _, v := range slice { v := v } 可省略显式复制(但传统 for init; cond; post 仍需手动处理);
- 多变量场景统一处理:若需捕获 i, name, data 等多个变量,建议封装为结构体或统一用局部副本,避免混合引用与值语义;
- 静态检查辅助:启用 go vet 或 staticcheck(如 SA9003: loop variable captured by func literal)可自动检测此类问题;
- 生命周期意识:被闭包捕获的变量会延长其生命周期(直到闭包不可达),不当使用可能导致内存泄漏。
? 总结
Go 的闭包按引用捕获变量是其函数式能力的基础,但也是并发陷阱的温床。理解 i := i 这行看似冗余的代码背后,是对 Go 内存模型与执行模型的深刻把握。在循环启动 goroutine 时,养成“显式创建值副本”的肌肉记忆,是编写健壮、可维护并发代码的第一道防线。

















