goroutine 启动后无法保证执行完成,必须显式同步;主goroutine退出程序即终止,需用sync.WaitGroup、通道或context.Context等待,其中WaitGroup适用于等待多个同类任务完成,须在启动前Add、结束前Done、全部启动后Wait。
Go 中 go 关键字启动的 goroutine 是异步且不可控生命周期的——主 goroutine 退出,整个程序立即终止,不会等待其他 goroutine。这是新手最常踩的坑:写完 go doWork() 就直接 return 或函数结束,结果 doWork 根本没机会执行。
time.Sleep 不是解决方案,它不可靠、难维护、掩盖真实依赖关系sync.WaitGroup、或带取消能力的 context.Context
当你要启动多个同类任务并等待它们全部完成时,sync.WaitGroup 是最直接的选择。注意它只计数,不传递数据,也不处理 panic。
wg.Add(1),不能在 goroutine 内部调用(竞态风险)wg.Done() 必须在 goroutine 结束前调用,建议用 defer wg.Done() 避免遗漏wg.Wait() 是阻塞调用,应放在所有 go 启动之后、且主逻辑需等待的位置var wg sync.WaitGroupfor i := 0; i < 3; i++ { wg.Add(1) go func(id int) { defer wg.Done() fmt.Printf("goroutine %d done\n", id) }(i) } wg.Wait() // 主 goroutine 在这里阻塞,直到所有子 goroutine 调用 Done
当 goroutine 需要返回结果、或主逻辑需响应其完成/失败时,channel 是更自然的选择。它把“完成”变成可读取的事件,也天然支持超时和 select 多路复用。
done := make(chan struct{}, 1)
或 select 等待
context.Context 检查 ctx.Done()
done := make(chan struct{}, 1)
go func() {
defer close(done) // 或 send: done <- struct{}{}
time.Sleep(100 * time.Millisecond)
}()
<-done // 等待完成最常见的泄漏模式是启动一个无限循环 goroutine,但没提供任何退出信号。例如:for { select { case —— 如果 ch 被关闭,select 会一直零消耗空转,goroutine 永不退出。
ctx.Done() 或检查 channel 是否已关闭pprof 查看 /debug/pprof/goroutine?debug=2 可快速定位堆积的 goroutine真正安全的常驻协程写法:
go func(ctx context.Context) {
for {
select {
case <-ch:
// 处理消息
case <-ctx.Done():
return // 收到取消信号,主动退出
}
}
}(ctx)协程退出不是语法问题,而是责任归属问题:谁启动,谁负责确保它有明确的生命周期边界。漏掉这一环,程序越跑越慢、内存越占越多,问题往往在压测或上线后才暴露。