go闭包捕获的是循环变量的内存地址而非值,因i在整个循环中复用同一栈空间,所有闭包共享该地址,执行时i已定格为终值;须用i := i或传参创建独立副本。

为什么闭包里读到的总是循环末值
Go 闭包捕获的是变量的地址,不是值。for i := 0; i 中的 <code>i 是同一个栈变量,每次迭代只是改它的值。所有闭包(比如 go func() { fmt.Println(i) }() 或 defer func() { log.Println(i) }())都指向这个地址。等它们真正执行时,循环早已结束,i 已是 5(或 4 后自增),输出全是 5 或 4。
go vet 会直接报 loop variable i captured by func literal,这是最该信任的信号。
用 i := i 创建独立绑定
这不是冗余赋值,而是作用域隔离的关键操作:每次迭代新建一个同名局部变量,它有自己的内存地址和初始值。
- 写法:
for i := 0; i - 效果:每个 goroutine 捕获的是自己那轮的
i副本,互不干扰 - 验证方式:
fmt.Printf("%p", &i)在循环内打印,会看到每次地址都不同 - 开销极小:对
int、指针、小结构体,只是栈上新分配,无拷贝成本
用函数参数传值更显式
把当前值作为参数传入闭包,语义上就是“此刻快照”,比遮蔽变量更直白,也更容易被静态分析工具识别。
- 正确写法:
go func(val int) { fmt.Println(val) }(i) - 常见错误:
go func(val int) { ... }(i)后漏掉(i)→ 编译失败;或写成defer func() { ... }()(i)→ 多了一对括号,变成立即执行 - 适用场景:尤其适合
http.HandleFunc、cron.AddFunc等要求func()类型但实际需带参的接口——此时必须包装一层,不能直接用func(i int) { ... }(i),因为调用结果不是函数类型
复合类型要小心底层共享
即使写了 item := item,若 item 是切片、map、指针或含指针字段的结构体,闭包拿到的仍是引用,后续迭代可能修改其底层数据。
- 例如:
for _, s := range []*string{&a, &b} { s := s; go func() { println(*s) }() }—— 如果a或b后续被改,输出仍会变 - 解决办法:需要深拷贝(如
copy切片、json.Marshal/Unmarshal序列化)或加同步控制(如sync.RWMutex) - 简单判断:如果变量本身是引用类型(
slice、map、chan、*T),就不能只靠:=遮蔽
真正容易被忽略的是这点:遮蔽只解决变量绑定问题,不解决数据共享问题。值类型安全,引用类型还得看内容。











