
本文详解 Go 循环中直接闭包捕获循环变量(如 i, j)引发的竞态行为——goroutine 实际读取的是变量最终值而非迭代快照,导致输出错乱;核心问题与互斥锁无关,而在于变量作用域与生命周期的误用。
本文详解 go 循环中直接闭包捕获循环变量(如 `i`, `j`)引发的竞态行为——goroutine 实际读取的是变量最终值而非迭代快照,导致输出错乱;核心问题与互斥锁无关,而在于变量作用域与生命周期的误用。
在 Go 中,当在 for 循环内启动 goroutine 并直接引用循环变量(如 i 或 j)时,这些变量并非按每次迭代独立复制,而是被所有 goroutine 共享同一内存地址。由于 goroutine 是异步调度执行的,而循环本身运行极快,往往在 goroutine 真正开始执行前,外层循环早已结束——此时 i 和 j 已达到终值(i = 5, j = 5),导致大量 goroutine 打印出 5 这样的“越界”值。
以原代码为例:
for i := 1; i < 5; i++ {
for j := 1; j < 5; j++ {
mutex.Lock()
go func() { // ❌ 错误:闭包捕获外部变量 i, j 的地址
fmt.Printf("%d + %d = %d\n", i, j, j+i)
mutex.Unlock()
}()
}
}-
i最终为5(循环条件i 失败后退出,<code>i值仍为5); -
j同样在内层循环末次递增后变为5,随后循环终止; - 所有 goroutine 在后续执行时读取的都是这个“过期”的
i=5、j=5,或因调度时机不同读取到中间状态(如i=4,j=5),造成输出中出现2 + 5 = 7、3 + 5 = 8等非预期组合; - 同时,因
i和j变量被快速覆盖,部分 goroutine 甚至来不及执行就被覆盖,导致i=1仅输出 3 行(而非预期的 4 行),本质是竞态下的执行丢失。
✅ 正确做法是:将当前迭代的变量值显式传入 goroutine,通过函数参数实现值拷贝,确保每个 goroutine 拥有独立、确定的副本:
package main
import (
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex // 推荐使用零值初始化,更符合 Go 习惯
for i := 1; i < 5; i++ {
for j := 1; j < 5; j++ {
mutex.Lock()
// ✅ 正确:立即传入 i, j 的当前值,形成闭包参数快照
go func(i, j int) {
fmt.Printf("%d + %d = %d\n", i, j, i+j)
mutex.Unlock()
}(i, j) // 注意:调用括号紧随 func 定义之后
}
}
// ⚠️ 注意:实际运行需等待所有 goroutine 完成(此处为简化未加 sync.WaitGroup)
// 生产环境必须使用 WaitGroup 或 channel 控制同步,否则主 goroutine 可能提前退出
}? 关键要点总结:
-
Mutex 不解决变量捕获问题:
sync.Mutex仅保护临界区并发访问,无法约束闭包对共享变量的读取时机; -
循环变量是单一实例:Go 中
for的i,j在整个循环中复用同一内存位置,不是每次迭代新建; -
参数传递即快照:
go func(i,j int){...}(i,j)中,i,j作为参数被复制进新 goroutine 栈帧,完全隔离; -
务必同步等待:示例中缺少同步机制,真实代码需配合
sync.WaitGroup阻塞主 goroutine,否则程序可能在 goroutine 打印前就退出。
遵循“显式传参”原则,即可彻底规避循环内 goroutine 的变量捕获陷阱,写出可预测、线程安全的并发代码。

















