goroutine 启动后无法保证执行完成,必须显式同步;主goroutine退出程序即终止,需用sync.WaitGroup、通道或context.Context等待,其中WaitGroup适用于等待多个同类任务完成,须在启动前Add、结束前Done、全部启动后Wait。

goroutine 启动后无法保证执行完成,必须显式同步
Go 中 go 关键字启动的 goroutine 是异步且不可控生命周期的——主 goroutine 退出,整个程序立即终止,不会等待其他 goroutine。这是新手最常踩的坑:写完 go doWork() 就直接 return 或函数结束,结果 doWork 根本没机会执行。
- 除非明确设计为“火种协程”(如后台监听),否则几乎所有业务 goroutine 都需要某种形式的等待机制
-
time.Sleep不是解决方案,它不可靠、难维护、掩盖真实依赖关系 - 真正可控的方式只有三类:通道通信、
sync.WaitGroup、或带取消能力的context.Context
用 sync.WaitGroup 等待一组 goroutine 退出
当你要启动多个同类任务并等待它们全部完成时,sync.WaitGroup 是最直接的选择。注意它只计数,不传递数据,也不处理 panic。
- 必须在启动 goroutine 前调用
wg.Add(1),不能在 goroutine 内部调用(竞态风险) -
wg.Done()必须在 goroutine 结束前调用,建议用defer wg.Done()避免遗漏 -
wg.Wait()是阻塞调用,应放在所有go启动之后、且主逻辑需等待的位置
var wg sync.WaitGroup
for 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用 channel 实现 goroutine 退出通知与结果传递
当 goroutine 需要返回结果、或主逻辑需响应其完成/失败时,channel 是更自然的选择。它把“完成”变成可读取的事件,也天然支持超时和 select 多路复用。
- 不要用无缓冲 channel 做信号通知(易死锁),推荐带长度 1 的缓冲 channel:
done := make(chan struct{}, 1) - 发送方应确保只发一次(尤其在 error 分支中也要发),接收方用
或select等待 - 若 goroutine 可能长期运行,且需支持外部中断,必须配合
context.Context检查ctx.Done()
done := make(chan struct{}, 1)
go func() {
defer close(done) // 或 send: done <- struct{}{}
time.Sleep(100 * time.Millisecond)
}()
<-done // 等待完成goroutine 泄漏:没有退出路径的常驻协程
最常见的泄漏模式是启动一个无限循环 goroutine,但没提供任何退出信号。例如:for { select { case —— 如果 ch 被关闭,select 会一直零消耗空转,goroutine 永不退出。
- 所有 for-select 循环都应有明确退出条件,比如监听
ctx.Done()或检查 channel 是否已关闭 - 不要依赖“程序退出时自动清理”,goroutine 不会自动回收资源(如打开的文件、网络连接)
- 用
pprof查看/debug/pprof/goroutine?debug=2可快速定位堆积的 goroutine
真正安全的常驻协程写法:
go func(ctx context.Context) {
for {
select {
case <-ch:
// 处理消息
case <-ctx.Done():
return // 收到取消信号,主动退出
}
}
}(ctx)协程退出不是语法问题,而是责任归属问题:谁启动,谁负责确保它有明确的生命周期边界。漏掉这一环,程序越跑越慢、内存越占越多,问题往往在压测或上线后才暴露。










