sync.WaitGroup 仅负责计数与等待,关键在并发启动与安全收尾:Add 必须在 goroutine 启动前调用,不可在 goroutine 内或并发调用,避免漏加、多加、竞态及闭包变量捕获错误。

sync.WaitGroup 不是为“语言学习任务”设计的,它只负责计数与阻塞等待——你传给它的每个 goroutine,不管是在翻译句子、调用 LLM API 还是解析语法规则,它都一视同仁:加一、减一、归零即放行。关键不在“学什么”,而在“怎么并发启动 + 怎么安全收尾”。
WaitGroup.Add 必须在 goroutine 启动前调用
常见错误是把 wg.Add(1) 放进 go func() { ... }() 里面,或者漏掉、多调、在循环外一次性 Add(len(tasks)) 却中途跳过某些 task。结果不是 wg.Wait() 永远卡住(计数器始终为 0),就是 panic 报 sync: WaitGroup misuse: Add called concurrently with Wait。
正确姿势:
- 循环中每个任务启动前立刻
wg.Add(1),且确保无竞态 - 若任务列表已知长度,也可在循环外统一
wg.Add(len(tasks)),但必须保证后续每个 task 都确实启动了 goroutine - 绝不在 goroutine 内部调用
Add
闭包捕获循环变量导致所有 goroutine 打印同一个 ID
写批量处理时容易这么写:
for i := 0; i < len(tasks); i++ {
wg.Add(1)
go func() {
defer wg.Done()
process(tasks[i]) // ❌ i 是共享变量,最后值为 len(tasks)
}()
}
结果所有 goroutine 都处理 tasks[len(tasks)-1],甚至 panic 索引越界。根本原因是匿名函数捕获的是变量 i 的地址,而非当前值。
立即学习“go语言免费学习笔记(深入)”;
修复方式只有一种:显式传参
for i := 0; i < len(tasks); i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
process(tasks[idx]) // ✅ 正确
}(i)
}
Done 调用不匹配会 panic 或死锁
wg.Done() 必须和 wg.Add(1) 严格一一对应。漏调 → wg.Wait() 永不返回;多调 → panic sync: negative WaitGroup counter。
典型风险点:
- 任务出错提前 return,但没走
defer wg.Done() - 用了多个
return分支,只在一处写了defer - 在循环里误写成
for ... { wg.Done() },重复调用
最稳妥写法永远是:defer wg.Done() 放在 goroutine 函数第一行,且该函数只做一件事(封装 task 处理逻辑)。
WaitGroup 无法感知任务是否真正退出
wg.Wait() 返回只表示计数器归零,不代表所有 goroutine 已调度完毕或内存已释放。比如你在 goroutine 里启了一个子 goroutine 做异步日志上报,主 goroutine 在 wg.Wait() 后就退出了,那个日志 goroutine 可能还在跑,甚至 panic。
若需强一致性退出,必须配合:
-
context.Context传递取消信号 - 额外的
done chan struct{}等待子 goroutine 显式关闭 - 避免在 goroutine 内再启未受控的 goroutine
批量语言学习任务常涉及 HTTP 请求、文件读写、模型推理——这些操作本身可能超时或阻塞,sync.WaitGroup 对它们完全无感。它只是个计数器,不是执行控制器。


















