WaitGroup 必须配合 goroutine 使用,单独调用 Wait() 会永久阻塞;Add() 必须在 go 语句前调用,Done() 应用 defer 确保执行,否则计数器异常导致 hang 或 panic。

WaitGroup 必须和 goroutine 配合使用,单独调用 Wait() 会永久阻塞
这是最常见的卡死原因:误以为 WaitGroup 能“自动感知”异步任务结束。它不会——Wait() 只是等待内部计数器归零,而计数器增减完全靠你手动调用 Add() 和 Done()。如果没调 Add(1) 就 Wait(),或漏掉 Done(),程序就 hang 在那里。
正确姿势是:在启动 goroutine 前调 Add(1),在 goroutine 结束前调 Done()(通常用 defer):
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done() // 确保执行完才减计数
time.Sleep(100 * time.Millisecond)
fmt.Println("done")
}()
wg.Wait() // 这里才真正同步等待多个 goroutine 场景下,Add() 必须在 go 语句之前调用
因为 Add() 操作不是原子的,如果写成 go func() { wg.Add(1); ... }(),就可能出现:goroutine 启动后还没来得及 Add(),主线程已执行到 Wait() 并发现计数为 0,直接返回——导致后续 Done() 调用 panic(“negative WaitGroup counter”)。
安全写法只有这一种顺序:
立即学习“go语言免费学习笔记(深入)”;
- 先
wg.Add(n)(n 是你要启的 goroutine 总数) - 再循环启动 n 个 goroutine,每个都带
defer wg.Done() - 最后
wg.Wait()
错误示例(会 panic):
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
for i := 0; i < 3; i++ {
go func() {
wg.Add(1) // ❌ 危险!并发修改计数器
defer wg.Done()
// ...
}()
}
wg.Wait()不能复用已 Wait() 过的 WaitGroup 实例
sync.WaitGroup 不是状态机,它内部计数器归零后,Wait() 返回,但之后不能再调 Add() ——Go 1.20+ 会 panic:“sync: negative WaitGroup counter”。旧版本可能静默失败或崩溃。
如果你需要多次等待不同批次任务,要么:
- 每次新建
sync.WaitGroup{}实例 - 改用
sync.Once+ channel 或errgroup.Group(更推荐)
别试图重置计数器——没有公开 API 支持。
替代方案:errgroup.Group 更适合带错误传播的异步转同步
纯等完成用 WaitGroup 没问题,但一旦涉及错误收集、上下文取消、或想避免手动管理计数器,errgroup.Group 是更健壮的选择:
g, ctx := errgroup.WithContext(context.Background())
for i := 0; i < 3; i++ {
i := i
g.Go(func() error {
select {
case <-time.After(100 * time.Millisecond):
return fmt.Errorf("task %d failed", i)
case <-ctx.Done():
return ctx.Err()
}
})
}
if err := g.Wait(); err != nil {
log.Printf("at least one task failed: %v", err)
}它自动处理计数、支持上下文、聚合第一个非-nil 错误——这些恰恰是手写 WaitGroup 容易漏掉的点。
WaitGroup 的计数器操作本身不加锁,但必须严格遵循“Add before go, Done in goroutine, Wait after all go”这个顺序;任何偏差都会立刻暴露为 panic 或死锁。实际项目里,越早换成 errgroup 或 context 驱动的模式,越少踩坑。

















