为什么协程会泄漏
Goroutine 本身很轻,但不是没有成本。常见泄漏并不是忘记调用某个关闭函数,而是协程永远阻塞在发送、接收、锁等待或不可取消的 I/O 上。服务运行时间越长,泄漏会逐渐表现为内存上涨、调度延迟增加,最终拖慢整个进程。
让取消信号贯穿调用链
入口请求创建的 Context 应继续传递到业务层、仓储层和外部调用。不要在中途重新使用 context.Background(),否则上游取消后,下游任务仍会继续执行。
func worker(ctx context.Context, jobs <-chan Job) error { for { select { case <-ctx.Done(): return ctx.Err(); case job, ok := <-jobs: if !ok { return nil }; if err := handle(ctx, job); err != nil { return err } } } }
发送端也必须可取消
只在消费端监听 ctx.Done() 仍然不够。生产者向无缓冲通道发送时,如果消费者已经退出,生产者同样会永久阻塞。发送操作也应放入 select,并在任务组结束后由拥有通道的一方关闭通道。
- 使用 errgroup.WithContext 统一管理同一任务内的协程。
- 为数据库、HTTP、gRPC 调用设置明确超时。
- 不要在子协程中吞掉错误,应将第一个错误返回任务组。
- 通过 runtime.NumGoroutine、pprof 和压测前后对比确认是否泄漏。
落地检查
评审并发代码时,可以依次检查:谁创建协程、谁负责结束、阻塞点是否可取消、通道由谁关闭、错误如何返回。只要这五个问题都有明确答案,大多数协程泄漏都能在上线前被发现。
评论
0 条讨论