Add必须在go语句前调用,因为WaitGroup仅依赖计数器预注册,不感知goroutine实际运行状态;若Add滞后,wg.Wait()可能因计数器为0而立即返回,导致主goroutine提前退出、任务未执行。

sync.WaitGroup.Add() 必须在 go 语句之前调用,否则主 goroutine 很可能提前退出,任务根本没跑完。
为什么 Add 必须写在 go 前面?
WaitGroup 的计数器是“预注册”机制:它不感知 goroutine 是否真在运行,只认 Add() 调用次数。主 goroutine 执行到 wg.Wait() 时,如果计数器仍是 0,就立刻返回——不管后面有多少个 go func() { wg.Done() } 正在排队或刚启动。
- 现象:
程序秒退、日志只打了一两行、结果为空但无 panic - 典型错误写法:
go func() { wg.Add(1); defer wg.Done(); ... }()—— 此时Add()和Done()都在子 goroutine 里,主流程完全不知道有任务要等 - 本质问题:goroutine 启动是异步的,
go语句返回不等于函数体已执行;而Add()放在go后,就失去了“声明任务存在”的时机
循环启动多个 goroutine 时怎么配 Add?
每个 go 对应一次 wg.Add(1),位置必须紧挨着 go 语句之前,且不能漏、不能多、不能复用变量。
- 正确模式:
wg.Add(1); go func(arg T) { defer wg.Done(); ... }(arg) - 危险模式(闭包陷阱):
for _, v := range items { wg.Add(1); go func() { fmt.Println(v) }() }—— 所有 goroutine 共享同一个v,可能少启或多启,导致Add次数和实际 goroutine 数不一致 - 安全写法:显式传参,如
go func(x string) { ... }(v),或用索引items[i]避免闭包捕获
Done() 为什么推荐用 defer?
defer wg.Done() 是防漏的底线措施,但不是万能的——它只对正常返回有效,panic 会跳过 defer。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- 分支多、return 点分散时,手动写
wg.Done()极易遗漏,造成Wait()永久阻塞 - panic 场景下,
defer不触发 → 计数器卡死 → 主 goroutine 卡住,无错误、无日志、无声无息 - 稳妥做法:用
recover包一层,确保无论是否 panic 都调用wg.Done(),例如:go func() { defer func() { if r := recover(); r != nil { log.Printf("panic: %v", r) } wg.Done() }() // 可能 panic 的业务逻辑 }()
WaitGroup 传参必须用 *sync.WaitGroup
值传递会复制整个结构体,包括内部的 mutex 和计数器字段,导致子 goroutine 的 Done() 和主 goroutine 的 Wait() 完全脱节。
- 错误:
func worker(wg sync.WaitGroup)或w2 := wg——w2.Done()修改的是副本,主wg计数器不变 - 正确:
func worker(wg *sync.WaitGroup),调用时传&wg - 结构体字段存 WaitGroup?不行。必须是
*sync.WaitGroup,初始化用new(sync.WaitGroup)或取地址
最隐蔽的坑不在语法,而在“以为它能动态注册”——WaitGroup 不是监听器,它只认你提前声明的数字。一旦漏加、错加、传错指针,表现不是 crash,而是静默失效:程序看似跑完,实则任务丢弃或等待悬停。

















