waitgroup需手动管理计数:add必须在go前调用,done须在子goroutine内defer调用且仅一次,禁止复制、重置或跨goroutine调用,否则导致死锁、panic或提前返回。

WaitGroup 的计数不是“自动感知任务”的,而是靠你手动登记和确认的——Add 在前、Done 在后、数量必须对得上,否则要么 Wait 立刻返回,要么永久卡住。
wg.Add() 必须在 go 语句之前调用
这是最常触发 sync: negative WaitGroup counter 或 Wait 提前返回的根源。主 goroutine 执行顺序不等于代码书写顺序,一旦 wg.Add(1) 被写在 go 后面,或和 go 并列但没加内存屏障,就可能:主 goroutine 已执行到 wg.Wait(),而子 goroutine 还没来得及调 Add,此时计数器仍是 0,Wait() 直接返回;或者部分 Add 晚到,导致计数器始终 >0,Wait() 死锁。
- ✅ 正确:循环中先
wg.Add(1),再go func() { defer wg.Done() }() - ❌ 错误:
go func() { wg.Add(1); ...; wg.Done() }()—— 动态登记,无法保证主 goroutine 看到新增 - ⚠️ 危险:
go func() { defer wg.Done() }(); wg.Add(1)—— 主 goroutine 可能已进入Wait()
Done() 必须由子 goroutine 自己调用且只调一次
Done() 是 “我完成了” 的声明,不是外部代劳的指令。它必须出现在每个子 goroutine 内部,并覆盖所有退出路径(包括 panic、error return、提前 return)。否则就会出现计数器不归零,Wait() 永远阻塞。
- ✅ 推荐:在子 goroutine 函数开头就写
defer wg.Done(),panic 时也触发 - ❌ 禁止:
wg.Done()写在主 goroutine 里,或在多个地方重复调用(比如嵌套defer) - ⚠️ 高危:
wg.Add(-1)或defer wg.Add(-1)—— 不是线程安全减法,且违反语义契约
WaitGroup 零值可用,但禁止任何形式的复制
var wg sync.WaitGroup 是完全合法的,但只要你把它赋值给另一个变量、传进函数时不传指针、或作为 struct 字段被拷贝,就立刻出问题。Go 编译器会在 go vet 阶段报 copy of sync.WaitGroup contains noCopy field,运行时则表现为 Wait() 死锁且无提示——因为副本上的 Done() 对原实例计数器毫无影响。
- ✅ 安全用法:始终传递
*sync.WaitGroup,例如worker(url string, wg *sync.WaitGroup) - ❌ 危险用法:
w2 := wg、doSomething(wg)(参数是值类型)、struct{ wg sync.WaitGroup } - ⚠️ 注意:Go 1.21+ 对隐式拷贝更敏感,比如
append([]MyStruct{{wg: wg}}, s)也可能触发 vet 报警
Wait() 返回后不能复用,也不支持重置
Wait() 只表示“这次登记的任务全部完成了”,不代表对象可再次使用。Go 官方明确不支持重置计数器,也没有 Reset() 方法。强行在 Wait() 后再调 Add(),Go 1.21+ 会直接 panic:sync: WaitGroup is reused before previous Wait has returned。
- ✅ 正确做法:每个并发批次用独立变量,如
wg1 := sync.WaitGroup{}、wg2 := sync.WaitGroup{} - ❌ 常见误用:
wg = sync.WaitGroup{}—— 旧wg可能还在被 goroutine 访问,竞态风险极高 - ⚠️ 补充:需要超时控制?不能把
Wait()放进select,得用done := make(chan struct{})+ 单独 goroutine 调wg.Wait()+close(done),再select等done或ctx.Done()
最易被忽略的一点:WaitGroup 只管数字归零,不管错误、顺序、超时、取消。它只是一个计数器+阻塞原语,别指望它帮你做别的事——该自己处理 error 就处理,该传 context 就传,该建新 wg 就建新 wg。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











