i总是最终值是因为go for循环复用同一内存地址,闭包捕获的是变量引用而非值;所有迭代共享该地址,循环结束后i定格为终值,同步或异步执行均输出相同结果。

闭包捕获循环变量时,i 为什么总是最终值
因为 Go 的 for 循环复用同一个变量内存地址,所有闭包共享该变量的引用,而非每次迭代创建新变量。即使循环结束前 i 已被赋值为 3,后续执行的闭包(如 goroutine 或延迟调用)读到的仍是这个终值。
这不是并发问题,纯同步代码也会复现:
for i := 0; i
- 根本原因:迭代变量
i是单个绑定,每次只是改写其值 - 闭包捕获的是
i的地址,不是当前值的副本 - Go 1.22+ 对
range中的值变量做了复制优化,但显式for变量行为未变,不可依赖
如何让每次迭代的闭包拿到正确的 i 值
只有两种可靠方式,本质都是切断对原循环变量的引用:
-
传参捕获(推荐):把当前值作为参数传入匿名函数,求值发生在调用时刻
go func(val int) { fmt.Println(val) }(i) -
循环内重声明:在循环体中用
i := i创建新局部变量for i := 0; i
注意:i := i 不是冗余赋值,而是变量遮蔽(shadowing),它为每次迭代分配独立栈空间;而 var i = i 语法错误,go func(i int) 必须立即调用 (i),否则仍捕获外层 i。
闭包延长变量生命周期的真实代价
只要闭包还存活,它捕获的变量就无法被 GC 回收——哪怕外层函数早已返回。这容易引发内存泄漏,尤其在高频场景下:
- HTTP handler 中闭包捕获整个
*http.Request,可能让请求体长期驻留堆上 - 循环中把闭包存入切片或 map 后未清理,所有被捕获的变量持续滞留
-
defer闭包捕获大结构体,触发逃逸分析,强制抬升到堆
验证方法:用 go build -gcflags="-m -l" 查看是否出现 ... moved to heap;若小变量也被抬升,大概率是闭包捕获了它所属的大对象(比如只用了 user.ID,却捕获了整个 user)。
defer 里的闭包也遵循同样规则
defer 闭包读取的是执行时变量的当前值,不是注册时的快照。常见陷阱:
for i := 0; i <p>修复方式与普通闭包一致:</p>
- 立即传参:
defer func(val int) { fmt.Print(val, " ") }(i) - 循环内重声明:
i := i; defer func() { fmt.Print(i, " ") }()
命名返回值更要小心:defer func() { err = fmt.Errorf(...) }() 会直接修改返回值,且延长 err 生命周期——这种隐式副作用容易被忽略。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











