go 中 for 循环变量复用同一内存地址,不会每次迭代新建变量;例如 for i := 0; i
for 循环变量在 Go 中是复用的,不是每次迭代新建
Go 的
for语句不会为每次迭代分配新变量,而是反复使用同一块栈内存。比如for i := 0; i 中的 <code>i始终是同一个地址;每次循环只是给这个地址写入新值。当你在循环体里写go func() { fmt.Println(i) }(),所有匿名函数捕获的都是这个地址——而不是某次迭代时的值。协程启动是异步的,主 goroutine 很快跑完循环(微秒级),
i已变成5;等那些 goroutine 真正调度执行时,读到的自然全是5。这不是“延迟”或“调度慢”,是确定性的内存复用行为。闭包捕获的是变量地址,不是值快照
Go 的匿名函数闭包对循环变量的引用是**按引用捕获**,这点和 JavaScript、Python 等语言一致。它不自动做深拷贝或快照,也不感知“当前迭代”。只要变量没逃逸出作用域,它就一直指向那个物理地址。
- 错误写法:
go func() { use(i) }()→ 所有 goroutine 共享&i- 正确本质:必须让每个 goroutine 持有独立的值副本,而非共享引用
两种安全写法的区别与适用场景
方案一:参数传值(推荐)
go func(val int) { use(val) }(i)
优点:语义清晰,参数名可自解释,IDE 和静态分析工具能更好识别意图;适合需要多个变量或复杂逻辑的场景。方案二:循环内重声明(简洁但易误读)
i := i再go func() { use(i) }()
本质是创建同名局部变量,触发值拷贝。注意:这行必须放在go之前,且不能和外层i混淆作用域;多人协作时容易被当成冗余代码删掉。两者都避免了数据竞争,但参数方式更利于后期扩展(比如加日志、加 context、加 error 处理)。
不手动拷贝可能触发 -race 检测器报错
当循环变量是结构体字段或指针(如
for _, m := range members { go func() { db.Exec(..., m.ID) }() }),问题会升级为真实的数据竞争:m.ID在循环中被反复写,而 goroutine 并发读——go run -race会直接报Data race on field ...。即使变量是基础类型(
int、string),不拷贝也会导致逻辑错误,只是不一定会被-race捕获(因为无写冲突,只有读错值)。所以不能依赖检测器,得从编码习惯上杜绝。最常被忽略的一点:range 循环里的
v同样复用内存,哪怕你只用v.Name,只要没拷贝,就可能读到下一轮覆盖后的脏值。












