WaitGroup.Add必须在goroutine启动前调用,否则可能因计数器变负而panic;Done需与Add严格匹配且用defer确保执行;WaitGroup必须传指针避免复制;它不提供内存可见性保证,共享变量仍需额外同步。

WaitGroup.Add 必须在 goroutine 启动前调用
很多人把 Add 放在 goroutine 里,结果 Wait 永远不返回,或者 panic:「panic: sync: negative WaitGroup counter」。这是因为 Add 和 Done 都操作同一个计数器,而 Wait 会检查计数器是否 ≤ 0 —— 如果 Add 晚于 Done 执行(比如在 goroutine 内部),就可能先减后加,导致计数器变负。
正确做法是:所有 Add(n) 必须出现在 go 语句之前,且不能依赖 goroutine 内部逻辑判断是否调用。
- ✅ 正确:
wg.Add(1)→go func() { defer wg.Done(); ... }() - ❌ 错误:
go func() { wg.Add(1); defer wg.Done(); ... }() - ⚠️ 特别注意循环启动时:如果用
for i := range items,直接在闭包里用i会导致所有 goroutine 共享同一个i值,Add可能只执行一次或错乱 —— 应该提前拷贝:idx := i; wg.Add(1); go func() { defer wg.Done(); work(idx) }()
Done 调用必须与 Add 匹配,且不能重复或遗漏
Done 本质是 Add(-1),它不校验当前计数器值,只做原子减法。一旦多调用一次,计数器就变负,Wait 会 panic;少调用一次,Wait 就永远阻塞。
最稳妥的方式是用 defer wg.Done(),确保无论函数从哪条路径 return 或 panic,都能执行减法。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 推荐写法:
func worker(wg *sync.WaitGroup) { defer wg.Done(); doWork() } - ❌ 危险写法:
if err != nil { return }; wg.Done()—— 错误分支漏掉Done - ⚠️ 注意:不能在已
Wait返回后继续调用Done,否则 panic(即使 wg 已重用)
WaitGroup 不能被复制,必须传指针
sync.WaitGroup 是一个 struct,内部包含 state1 [3]uint32,属于非可复制类型。如果按值传递(比如作为函数参数不加 *),Go 会复制一份 —— 主 goroutine 的 Wait 等的是副本的计数器,而子 goroutine 调用的是原始 wg 的 Done,两者完全无关,Wait 永远不会返回。
这个错误在本地测试可能偶然“看起来正常”(尤其任务少、调度快),但上线后必现超时或 hang 住。
- ✅ 正确:
go worker(&wg),函数签名func worker(wg *sync.WaitGroup) - ❌ 错误:
go worker(wg),函数签名func worker(wg sync.WaitGroup) - ⚠️ IDE 通常会警告 “copying lock value”,但 Go 编译器本身不报错 —— 这是典型的静默陷阱
WaitGroup 不提供内存可见性保证,共享变量仍需额外同步
WaitGroup 只保证“所有 goroutine 已结束”,不保证主 goroutine 能看到它们对共享变量的修改。比如多个 goroutine 写入一个 map,然后 Wait 返回,此时读 map 可能 panic(并发写)或读到脏数据(缺少 memory fence)。
它解决的是“执行完成”的协调问题,不是“数据安全”的同步问题。
- ✅ 正确组合:
mu.Lock(); data[key] = val; mu.Unlock()+defer wg.Done() - ✅ 替代方案:用 channel 汇总结果,避免共享变量
- ⚠️ 常见误区:以为
Wait返回 = 数据已就绪 —— 实际上,除非用了sync.Mutex、atomic或 channel,否则读写仍可能竞争
Done 无提示,共享数据不保护。真正用稳的关键,是把它当成一个纯计数信号量,而不是万能同步器。


















