如何正确使用goroutine_Go协程创建与退出机制

9次阅读

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

如何正确使用 goroutine_Go 协程创建与退出机制

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 donen", 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:>

协程退出不是语法问题,而是责任归属问题:谁启动,谁负责确保它有明确的生命周期边界。漏掉这一环,程序越跑越慢、内存越占越多,问题往往在压测或上线后才暴露。

text=ZqhQzanResources