for循环中启动goroutine会打印最后一个值,因循环变量i被复用,所有goroutine共享同一地址,执行时循环已结束、i定格在终值,属变量作用域与内存复用的正常现象。

for 循环里起 goroutine 为什么总是打印最后一个值
因为 for 循环变量是复用的,所有 goroutine 共享同一个变量地址,等它们真正执行时,循环早已结束,i 或 v 已定格在终值。这不是 Go 的 bug,是变量作用域和内存复用的自然结果。
- 典型现象:
for i := 0; i 输出三个 <code>3 - 根本原因:匿名函数捕获的是变量
i的地址,不是它的值 - 最直接解法:在循环体内用局部变量“快照”当前值,比如
for i := 0; i - 更清晰写法:把值作为参数传入闭包,
go func(val int) { fmt.Println(val) }(i)—— 参数传递天然按值拷贝
range 遍历切片/Map 时启动 goroutine 的坑
range 的第二个返回值(如 v)也是循环变量,同样被复用。遍历 []string{"a","b","c"} 时,所有 goroutine 最终都看到 v == "c"。
- 错误写法:
for _, v := range strs { go func() { fmt.Println(v) }() } - 安全写法一(显式拷贝):
for _, v := range strs { v := v; go func() { fmt.Println(v) }() } - 安全写法二(传参):
for _, v := range strs { go func(val string) { fmt.Println(val) }(v) } - 注意:如果
v是大结构体,传参会复制,有性能顾虑时优先用v := v方式
sync.WaitGroup 配合闭包时 wait 不住的常见原因
WaitGroup 本身不解决闭包捕获问题,但错误用法会让 wg.Done() 没被执行,或执行时机错乱,导致主协程提前退出。
- 典型错误:
for i := 0; i —— 这里不仅 <code>i错,wg.Add(1)也可能在 goroutine 启动前就被后续循环覆盖(虽概率低,但逻辑脆弱) - 正确顺序:先
wg.Add(1),再启动 goroutine;且i必须做快照或传参 - 推荐组合:
for i := 0; i - 别在闭包里漏掉
defer wg.Done(),也别把它放在条件分支里——确保一定会执行
闭包捕获指针或结构体字段时的隐性风险
你以为捕获的是值,其实可能是地址;你以为字段不变,但它可能被其他 goroutine 并发修改。
立即学习“go语言免费学习笔记(深入)”;
- 例如:
type User struct{ Name string }; u := User{"Alice"}; go func() { fmt.Println(u.Name) }()—— 这里u是值拷贝,安全 - 但:
u := &User{"Alice"}; go func() { fmt.Println(u.Name) }()—— 所有 goroutine 共享同一块内存,u.Name可能被中途改写 - 更隐蔽的 case:
for _, u := range users { go func() { fmt.Println(u.Name) }() },即使users是[]*User,u仍是复用的指针变量,最终全打在最后一个元素上 - 判断依据:看闭包里引用的变量是否在循环中反复赋值;只要它出现在
:=右侧或range中,就默认要处理
真正麻烦的不是语法怎么写,而是你得时刻分辨:这个变量,此刻是栈上的独立一份,还是某个地址的别名。Go 不会替你做快照,它只忠实地按你写的符号去取值——哪怕那个符号下一秒就被覆盖了。










