闭包捕获的是变量地址 &i 而非值,因 Go 中 for 循环复用同一内存地址,循环结束后所有闭包读取的都是终值,同步代码亦如此。

闭包捕获的是 &i,不是 i 的值
Go 的 for 循环变量复用同一内存地址,所有迭代中的 i 都是同一个栈变量。闭包捕获的不是“当前值”,而是该变量的地址——也就是 &i。等 goroutine 或 defer 真正执行时,循环早已结束,i 的值固定为终值(比如 len(slice)),所有闭包读的都是这个终值。
这不是并发 bug,纯同步代码也会复现:
for i := 0; i < 3; i++ {
f := func() { fmt.Println(i) }
f() // 输出 3, 3, 3 —— 不是 0, 1, 2
}
- 根本原因:Go 不在每次迭代新建变量,只改写
i的值 - 闭包绑定的是变量地址,不是快照
- 哪怕没启动 goroutine,只要闭包延迟执行(如 defer、后续调用),问题一样出现
i := i 不是冗余,是显式创建新栈变量
写 i := i 是为了在每次迭代中强制分配一个独立的局部变量。它不是“复制值”那么简单,而是让编译器识别出:这是一个新绑定,有自己独立的内存地址。
示例:
立即学习“go语言免费学习笔记(深入)”;
for i := 0; i < 3; i++ {
i := i // 关键:声明新变量,值拷贝,地址独立
go func() {
fmt.Println(i) // 每个 goroutine 捕获的是自己的 <code>i</code> 地址
}()
}
- 不能写成
var i = i(语法错误) - 不能省略声明直接用
go func() { ... }(i)(参数未定义) - 对指针也有效:
ptr := &slice[i],闭包捕获的是这次迭代的ptr地址,而非循环变量本身
go func(i int) 形参传值为什么安全
函数形参是值传递,每次 go 语句执行时,都会把当前 i 的值拷贝进新栈帧。闭包捕获的,其实是这个形参变量的地址——而每个 goroutine 调用都有自己的形参栈空间。
for i := 0; i < 3; i++ {
go func(i int) { // i 是形参,每次调用都新建
fmt.Println(i) // 安全,输出 0 1 2
}(i) // 必须立即传入,否则闭包没触发,仍用外层 <code>i</code>
}
-
(i)不能省略:缺了它,函数就没被调用,闭包里读的还是外层循环变量 - 若函数体复杂或需复用,建议提取为独立函数,避免闭包嵌套过深
- 传大结构体时注意成本:值拷贝可能昂贵,此时应传指针 + 显式拷贝逻辑
逃逸分析必须实测,不能靠经验猜
闭包是否导致变量逃逸,取决于它是否被“外部持有”。小类型(如 int)在简单 defer 中可能不逃逸,但一旦涉及 &x、返回闭包、或 goroutine,基本都会逃逸到堆。
验证方式唯一可靠:
go build -gcflags="-m -l" main.go
- 看到
moved to heap: x或x escapes to heap→ 确认逃逸 - 加
-l是关键:禁用内联后,逃逸信息才准确定位到变量定义行 - 仅用
-m容易误判,逃逸信息会跑到调用处
真正容易被忽略的点是:逃逸不是“有没有”,而是“什么时候逃”——同一个变量,在 defer func() { println(x) }() 和 defer func() { println(&x) }() 中的命运完全不同。不看编译器输出,几乎无法准确预判。


















