如何在 Go 中利用 pprof 分析 Goroutine 泄露问题
最直观的方式,就是直接去看 /debug/pprof/goroutine?debug=2 的输出。如果 goroutine 的数量持续往上走,而且业务请求量稳定甚至为零的时候它还在缓慢攀升,那基本可以断定有泄露了。这里需要区分一下:活跃的 goroutine 正在执行,阻塞的 goroutine 卡
最直观的方式,就是直接去看 /debug/pprof/goroutine?debug=2 的输出。如果 goroutine 的数量持续往上走,而且业务请求量稳定甚至为零的时候它还在缓慢攀升,那基本可以断定有泄露了。这里需要区分一下:活跃的 goroutine 正在执行,阻塞的 goroutine 卡在 channel、锁或者 syscall 上——两者都是泄露的风险点,但后者更难揪出来。

pprof 里怎么看 Goroutine 数量是否异常
直接看 /debug/pprof/goroutine?debug=2 的输出是最直观的判断方式。如果数量持续增长且不回落,尤其在业务请求量稳定甚至为零时仍缓慢上升,基本可以确认存在泄露。注意区分「活跃 goroutine」和「阻塞 goroutine」:前者是正在执行的,后者可能卡在 channel、锁或 syscall 上——两者都算泄露风险点,但后者更难定位。
实操建议:
- 用
curl http://localhost:6060/debug/pprof/goroutine?debug=1 | wc -l快速统计行数(每行一个 goroutine),定时采集做趋势比对 ?debug=2输出带栈帧,但体积大;生产环境慎用,优先用?debug=1+go tool pprof离线分析- 对比多个时间点的
goroutineprofile,重点关注重复出现的调用路径,比如总在http.(*conn).serve或自定义的workerLoop里新建却没退出
为什么 runtime.GoroutineProfile 不够用
runtime.GoroutineProfile 只能获取当前快照,且要求提前调用 runtime.Stack 或手动触发 GC 才能拿到较全信息,无法反映增长趋势,也不支持 HTTP 接口式按需采集。它更适合嵌入测试逻辑中做断言,而非线上诊断。
实操建议:
- 不要在生产代码里循环调用
runtime.GoroutineProfile来“监控”,开销高且易误判(比如短生命周期 goroutine 正常波动) - 若必须程序内采集,改用
debug.ReadGCStats配合runtime.NumGoroutine()做轻量级水平告警,但仅作辅助 - 真正定位泄露,依赖的是 pprof 的 HTTP handler 提供的可复现、可归档、可 diff 的栈快照
如何用 go tool pprof 分析 goroutine 栈
拿到 goroutine?debug=2 的原始文本后,保存为 goroutines.txt,再用 go tool pprof 加载分析。虽然它本意是处理二进制 profile,但支持文本格式的 goroutine dump(从 Go 1.11+ 开始)。
实操建议:
- 命令:
go tool pprof -http=:8080 goroutines.txt,启动 Web UI 后点「Top」看最深栈或「Flame Graph」找高频分支 - 重点过滤自定义包名:在 Web UI 的 search 框输入
yourpackage.*,快速聚焦业务代码中的 goroutine 创建点 - 若发现大量相同栈(如都停在
select {}或ch <-),说明 channel 未被关闭或接收方已退出,发送方还在傻等
常见 Goroutine 泄露模式及修复线索
90% 的泄露来自三类场景:HTTP handler 启动 goroutine 但没加 context 控制、channel 使用不配对、timer/ticker 未 stop。pprof 显示的栈往往暴露了源头,但需要结合代码逻辑判断是否真的“该结束却没结束”。
实操建议:
- 检查所有
go fn()调用:是否传入了context.Context?是否在fn内部监听ctx.Done()并 clean up? - 检查 channel 操作:发送前是否确认接收方存活?是否用了
select { case ch <-:防阻塞?close(ch)是否只调一次且时机合理? - 检查
time.Ticker和time.Timer:是否在 goroutine 退出前调用了t.Stop()?Stop()返回false表示已触发,此时需额外处理已排队的事件
pprof 给你的是“谁没走”,不是“为什么没走”。真正的修复点往往藏在 goroutine 启动时传入的参数、闭包捕获的变量、以及它等待的 channel 或 timer 生命周期里——这些没法靠栈打印直接看出,得回代码里一行行对。


































