runtime.numgoroutine() 持续单向增长是协程泄漏最直接信号,需关注请求后不回落或长期单调上升趋势;结合 pprof 堆栈、goleak 测试拦截和 prometheus 长期监控可系统化定位泄漏。

用 runtime.NumGoroutine() 快速确认是不是真泄漏
协程数持续上涨是泄漏最直接信号,但别一看到数字变大就 panic——刚启动时 runtime.NumGoroutine() 跳到 80+ 很正常,那是 pprof、健康检查、日志采集等后台 goroutine 在初始化。真正要盯的是:单次请求处理完后总数不回落;或者服务稳定运行几小时后,数字从 50 → 200 → 800 这样单调斜线上升。
实操建议:
- 在 HTTP handler 入口和出口各打一行日志:log.Printf("goroutines: %d", runtime.NumGoroutine())
- 至少记录三个时间点:空闲态(压测前)、压测中、压测后 30 秒
- 不要只看一次值,趋势才是关键
用 net/http/pprof 抓阻塞 goroutine 的调用栈
引入 _ "net/http/pprof" 后,服务自动注册 /debug/pprof/goroutine 端点。访问 http://localhost:6060/debug/pprof/goroutine?debug=2 可看到全部 goroutine 的完整堆栈,包括阻塞位置、等待时长、创建源头。
常见错误现象:
- 几百个 goroutine 卡在 chan receive (nil chan):说明监听了未关闭的 channel
- 大量 goroutine 停在 io.ReadFull 或 ssh.Dial 后不再推进:网络连接没设超时或没做 context 控制
- 所有新增 goroutine 都卡在同一个 select 分支里:大概率漏了 default 或没响应 ctx.Done()
对比快照更有效:先抓一次 baseline,压测/跑一段时间后再抓一次,用文本 diff 工具比对,新增且长期阻塞的 goroutine 就是线索。
测试阶段用 goleak.VerifyNone() 拦住泄漏
线上才暴露问题太晚。在单元测试里加一行 defer goleak.VerifyNone(t),就能在测试结束时自动检查有没有“孤儿 goroutine”残留。
使用场景:
- 单个测试函数内启动 goroutine 的逻辑(比如模拟异步回调)
- 测试结束后,goleak 会报出类似这样的泄漏信息:Goroutine 35 in state chan receive(nil chan),并附上完整调用栈
- 如果整个包测试都希望统一检测,改用 goleak.VerifyTestMain(m) 放在 TestMain 里
注意坑:
- goleak 默认忽略标准库的 goroutine(如 http server),但如果你自己启了后台 ticker 或监听,得显式用 goleak.IgnoreTopFunction() 排除,否则误报
- 它不检测“缓慢泄漏”,只抓测试生命周期内未退出的 goroutine
长期监控靠 Prometheus + runtime.NumGoroutine()
有些泄漏增长极慢,一天涨 2–3 个,靠人工查根本发现不了。把 runtime.NumGoroutine() 暴露成 Prometheus 指标,配 Grafana 曲线图,一眼就能看出“发际线斜率”。
实操要点:
- 用 promauto.NewGaugeFunc 注册指标,避免重复注册导致 panic
- label 尽量精简,别加 pod name 这类高基数字段,否则指标爆炸
- 设置告警规则:比如 1 小时内增长 >50,或连续 3 小时斜率 >0.8
容易被忽略的一点:Prometheus 拉取指标本身会触发少量 goroutine,所以曲线底部会有轻微毛刺,别把它当泄漏信号——重点看是否突破历史水位线并持续抬升。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











