waitgroup 必须在启动 goroutine 前调用 add(1),否则可能因 done() 先执行导致计数器下溢 panic;需避免复用、跨作用域 wait,不捕获 panic,应配合 context 实现超时与取消。

WaitGroup 必须在启动 goroutine 前 Add
最常见的 panic 是 panic: sync: negative WaitGroup counter,根本原因是 WaitGroup.Add() 调用晚于 goroutine 启动。Go 运行时无法保证 goroutine 立即执行,但一旦某个 goroutine 先跑到 Done(),而计数器还没被 Add() 过,就会下溢。
正确做法是:在 go 语句前调用 Add(1),且不能用循环变量直接传给闭包(易捕获同一地址):
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1) // ✅ 必须在这里
go func(u string) {
defer wg.Done()
fetch(u)
}(url) // ❌ 不要传 &url,✅ 传值 url
}
不要在 WaitGroup 上做多次 Wait 或跨作用域复用
WaitGroup 不是可重入的,Wait() 返回后内部计数器已归零,再次调用 Wait() 会永久阻塞(除非后续有 Add())。更隐蔽的问题是:把 WaitGroup 作为函数参数传入并试图在多个地方 Wait(),容易因调用顺序错乱导致死锁。
- 每个逻辑任务应使用独立的
WaitGroup实例 - 避免将
WaitGroup作为结构体字段长期持有,除非你严格控制其生命周期 - 如果需要“分阶段等待”,改用
sync.Cond或多个WaitGroup组合
WaitGroup 无法捕获 panic,需手动 recover
WaitGroup 只负责计数和阻塞,不处理 panic。任一 goroutine panic 会导致整个程序崩溃,而主 goroutine 卡在 Wait() 上——你以为它还在等,其实已经崩了。
立即学习“go语言免费学习笔记(深入)”;
若需容错,必须在每个 goroutine 内部加 recover:
<pre class="brush:php;toolbar:false;">go func() {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panicked: %v", r)
}
}()
riskyOperation()
}()
注意:recover 只在 defer 中有效,且只捕获当前 goroutine 的 panic。
WaitGroup 和 context.WithTimeout 配合使用更健壮
仅靠 WaitGroup 无法应对超时场景。比如一批 HTTP 请求,某个卡死,Wait() 就永远不返回。此时应结合 context 控制整体时限:
- 用
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - 把
ctx传给每个子任务(如http.Client.Do(req.WithContext(ctx))) - 主 goroutine 同时监听
ctx.Done()和wg.Wait(),推荐用select+sync.Once触发 cancel
单独依赖 WaitGroup 的并发控制,在真实服务中基本不够用;超时、取消、错误传播这些必须由 context 补齐。










