for循环中直接在goroutine或defer中捕获循环变量i/v必然出错,因go复用同一内存地址,所有闭包共享终值;安全做法是传参(go func(x) {...}(i))或重声明(i := i)。

for 循环里直接用 i 或 v 启动 goroutine 必然出错
不是“可能出错”,是语言机制决定的必然行为:Go 的 for 循环复用同一块栈内存给 i 和 v,所有 go func() { fmt.Println(i) }() 捕获的都是这个地址。等 goroutine 真正执行时,i 早已是终值(比如 len(slice)),输出全是同一个数。
常见错误现象:
for i := 0; i → 输出 <code>3 3 3-
for _, v := range items { go func() { process(v) }() }→ 所有 goroutine 处理的是最后一个v的值 -
go vet会明确报loop variable i captured by func literal,别忽略它
defer 中引用循环变量也一样踩坑
defer 和 goroutine 共享同一套变量捕获规则,区别只在参数求值时机:defer fmt.Println(i) 的 i 是立即求值;而 defer func() { fmt.Println(i) }() 的 i 是延迟读取——所以后者同样输出终值。
典型错误写法:
-
for _, f := range files { defer os.Remove(f.Name()) }→ 只删了最后一个文件 for i := 0; i → 输出 <code>3 3 3- 配合
panic更隐蔽:defer log.Printf("err: %v", err)放太早,err还没被赋值就 panic 了
两种安全写法:传参优先,重声明次之
核心目标是让每次闭包绑定一个独立、不可变的值,而不是共享变量。两种方式都有效,但适用场景不同:
-
传参(推荐):
go func(idx int) { process(idx) }(i)—— 参数在go语句执行时立即求值,idx是独立副本,语义清晰,适合逻辑复杂或需传多个值的场景 -
重声明(简单场景可用):
i := i; go func() { process(i) }()—— 在当前作用域新建绑定,Go 编译器对这种短声明做了逃逸优化,性能无负担 - 别混用:
go func() { process(i) }(i)是语法错误;defer func() { ... }()后面不能跟(i),那是立即执行
指针、切片、map 这类复合类型更危险
很多人以为 v 是值类型就安全,但只要 v 本身含引用(比如 *T、[]byte、map[string]int),闭包捕获的仍是底层共享数据:
-
for _, v := range []*int{&a, &b} { go func() { *v = 42 }() }→ 所有 goroutine 修改同一个*v -
for _, v := range [][]byte{b1, b2} { go func() { append(v, 1) }() }→ 底层数组可能被并发修改,触发 panic - 安全做法统一走传参:
go func(val []byte) { process(val) }(v),或明确取元素地址:&items[i]
最易被忽略的点:哪怕你写了 v := v,如果 v 是指针或切片,解引用后仍可能读写同一块内存。传参不是万能解药,得看传进去的东西到底“值”还是“引用”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











