主 goroutine 过早退出会导致子任务丢失,必须用 sync.WaitGroup 显式等待;Add 必须在 goroutine 启动前调用,Done 应 defer 调用;需结果时应结合 channel 传递输出。

主 goroutine 过早退出导致子任务丢失
这是最常见也最容易被忽略的问题:主函数执行完就直接退出,所有未完成的 goroutine 被强制终止。Go 不会等待后台协程,哪怕它们正在写数据库或上传文件。
典型错误写法:
func main() {
for _, item := range items {
go process(item) // 没有同步机制
}
// 此处立即返回,process 可能一个都没跑完
}- 必须显式等待,不能依赖
time.Sleep—— 时间难估、不可靠、掩盖问题 -
sync.WaitGroup是标准解法,但要注意Add必须在go之前调用,否则计数器可能漏加 - 如果子任务需要返回结果或错误,
WaitGroup不够用,得搭配channel或sync.Once+ 共享变量
WaitGroup 计数器误用引发 panic
sync.WaitGroup 的 Add 和 Done 必须成对出现,且不能在 goroutine 内部调用 Add(除非你明确知道当前 goroutine 已启动并能确保计数稳定)。
常见错误模式:
立即学习“go语言免费学习笔记(深入)”;
for _, item := range items {
go func() {
wg.Add(1) // ❌ 危险!Add 在 goroutine 内,可能并发修改计数器
defer wg.Done()
process(item)
}()
}- 正确做法是:循环中先
wg.Add(1),再go启动 goroutine -
Done()建议用defer包裹,避免遗漏(尤其有多个 return 分支时) - 计数器为负会 panic:
panic: sync: negative WaitGroup counter,说明Done多于Add
需要结果汇总时,channel 与 WaitGroup 如何协作
单纯等完成不够,业务常需收集每个子任务的输出(如处理状态、错误、返回值)。这时不能只靠 WaitGroup,得引入 channel 传递数据。
推荐组合模式:
results := make(chan Result, len(items))
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
res := process(it)
results <- res // 非阻塞发送(因有缓冲)
}(item)
}
go func() {
wg.Wait()
close(results) // 所有 goroutine 完成后关闭 channel
}()
// 主 goroutine 从 results 读取直到关闭
for res := range results {
handle(res)
}- channel 缓冲大小设为
len(items),避免 sender 阻塞;若不确定数量,用无缓冲 +select+default防死锁 - 务必在
wg.Wait()后关闭 channel,否则range会永远阻塞 - 不要在 goroutine 内直接 close channel —— 多个 goroutine close 同一 channel 会 panic
超时控制与取消信号必须由 context 驱动
WaitGroup 本身不支持超时或取消。Wait() 会一直卡住,直到计数归零。生产环境必须加保护。
- 用
context.WithTimeout包裹主等待逻辑,配合select判断是否超时 - 把
ctx传入每个子 goroutine,在process中定期检查ctx.Err() - 避免用
time.After单独做超时 —— 它无法主动中断正在运行的 goroutine - 一旦超时,应尽快释放资源(如关闭 http.Client、cancel database query)
复杂点在于:超时后,已启动但未完成的 goroutine 不会自动停止,必须靠 context 显式协作退出。这点容易被忽略,导致 goroutine 泄漏。


















