结论:所有在for循环中直接捕获循环变量的goroutine必然出错,因变量复用内存地址;正确做法是传参或重声明变量。

直接说结论:所有在 for 循环里启动的 goroutine,只要闭包里直接用循环变量(比如 i 或 v),就一定会出错——不是“可能”,是“必然”。
为什么 goroutine 里打印的全是最后一个 i 值
根本不是 goroutine “跑得太慢”,而是 Go 的 for 循环变量复用内存地址。整个循环只有一块栈空间给 i,每次迭代只是改它的值;所有 go func() { fmt.Println(i) }() 捕获的都是这个地址,等真正执行时,i 早就是终值了(比如 len(slice))。
- 哪怕你把
go换成普通函数调用:func() { fmt.Println(i) }(),输出照样全是终值 -
range的i和v一样复用,别信“v 是副本就安全”这种说法 -
go vet会明确报loop variable i captured by func literal,别忽略它
goroutine 里传参 vs 变量重声明,怎么选
两种写法都有效,但语义和适用场景不同:
- 用参数传入:
go func(idx int) { process(idx) }(i)—— 参数在go语句执行时立即求值,idx是独立副本,最可控,推荐用于逻辑复杂或需显式传递多个值的场景 - 用变量重声明:
i := i; go func() { process(i) }()—— 在当前作用域新建绑定,i是新变量,Go 编译器对这种短声明做了逃逸优化,性能无负担,适合简单场景 - 别混用:
go func() { process(i) }(i)是语法错误,函数字面量不接受调用语法
range 中 v 是指针、切片、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]
defer 里也踩同样坑,但参数求值时机不同
defer 和 goroutine 共享同一套变量捕获规则,但有个关键区别:defer fmt.Println(i) 的 i 是立即求值;而 defer func() { fmt.Println(i) }() 的 i 是延迟读取。
- 错误写法:
for i := 0; i → 输出三个 <code>3 - 正确写法:
for i := 0; i → 输出 <code>0、1、2 - 资源清理场景(如
defer os.Remove(f.Name()))必须用f := f或传参,否则只删最后一个文件
最容易被忽略的点,不是“怎么修”,而是默认“每次循环都有个新 i”——Go 里没有这回事。变量复用是语言设计,不是 bug,得主动切断绑定。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











