协程泄漏即goroutine持续卡住不退出,铁证是runtime.numgoroutine()持续上涨且pprof debug=2显示大量goroutine阻塞在chan receive/select/semacquire/io wait;需排除初始化波动,重点监控请求后不回落、压测后不恢复、长期单调上升三种情形。

协程泄露就是 goroutine 启了却永远卡住、退不出,不是“可能有问题”,而是 runtime.NumGoroutine() 持续上涨 + pprof 里一堆卡在 chan receive/select/semacquire 的堆栈 —— 这就是铁证。
怎么一眼确认是不是真泄漏
别一看到 runtime.NumGoroutine() 数字高就慌。刚启动 HTTP 服务时跳到 120+,大概率只是 pprof、health check、log 等后台 goroutine 在初始化。
真正要盯的是这三种情形:
- 单次请求处理完,数字没回落(比如入口打点是 80,
handler返回后还是 80+) - 压测结束等 30 秒,总数仍比空闲态高一大截
- 稳定运行几小时后,数字呈单调上升(50 → 180 → 420)
建议在关键路径加三处日志:log.Printf("goroutines@idle: %d", runtime.NumGoroutine())、@start、@done —— 只采一次样没用,得看变化趋势。
pprof?debug=2 是线上唯一可靠入口
默认的 /debug/pprof/goroutine?debug=1 只给统计摘要,看不出谁卡在哪;必须用 ?debug=2 才输出完整调用栈。
生产环境直接 curl 就行:
curl "http://localhost:6060/debug/pprof/goroutine?debug=2"
重点关注这些状态的 goroutine:
-
chan receive(监听未关闭 channel) -
select(漏写default或所有case都阻塞) -
semacquire(锁没释放、WaitGroup没Done) -
IO wait(比如io.ReadFull卡住、SSH 连接没超时)
如果看到几百个 goroutine 全卡在同一个函数里、阻塞时间显示 “250000+ minutes”,不用犹豫,就是它。
goleak 是测试阶段最有效的拦截器
goleak 不适用于运行时监控,但它能在单元测试里当场揪出泄漏:启了个 time.Ticker 忘记 Stop(),或用 go func() { }() 启了闭包却等不到信号 —— 它都能捕获。
在 TestMain 或每个测试末尾加:
defer goleak.VerifyNone(t)
注意两点:
- 必须放在
defer里,否则 test panic 时不会执行,漏检 - 别用
goleak.IgnoreTopFunction("testing.(*T).Run")—— 这是忽略框架自身,不是你的代码
goleak 告诉你“有泄漏”,但不告诉你“在哪漏”。线上或集成测试中无法用它时,pprof 是唯一可靠入口。
修复核心:谁启的 goroutine,谁负责它能退出
90% 的泄漏来自 channel 使用不当。三个硬约束:
- 谁创建
channel,谁close;重复close或对nilchannelclose都会 panic - 谁接收,谁负责退出条件;
for range ch要求发送方必须close,否则永远挂起 - 谁启动 goroutine,谁确保它能结束 —— 不能只靠 defer wg.Done(),panic 时它根本不会执行
正确做法是用 context.Context 控制生命周期,或引入 quit chan struct{} 显式通知退出。例如 Walk() 场景里,提前 return 导致发送方还在发、接收方已退出,就得靠 select { case ch 来中断递归。
复杂点在于:goroutine 卡住不光是逻辑问题,它还会钉住栈内存(默认 2KB,可能扩到几 MB),甚至拖累堆上对象无法被 GC —— 所以泄漏早期可能看不到 heap profile 异常,但 goroutine profile 一定早就有迹可循。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











