快速确认 Goroutine 泄露需观察 /debug/pprof/goroutine?debug=1 的数字趋势,若请求稳定时 runtime.NumGoroutine() 持续单向增长即可能泄露;再结合 debug=2 输出中大量处于 chan receive、select 或 sync.WaitGroup.Wait 且阻塞时间达数天的 goroutine 进一步确认。

怎么快速确认 Goroutine 确实泄露了
直接看 /debug/pprof/goroutine?debug=1 返回的数字趋势——如果业务请求量稳定甚至为零时,runtime.NumGoroutine() 仍持续单向增长,基本就是泄露。更关键的是查 /debug/pprof/goroutine?debug=2 输出里有没有大量状态为 chan receive、select 或 sync.(*WaitGroup).Wait 的 goroutine,尤其当它们的阻塞时间显示为 432000s(5 天)、1209600s(14 天)这种量级,几乎可以断定不是慢操作,而是卡死。
为什么不能只靠 runtime.GoroutineProfile
runtime.GoroutineProfile 只能抓当前快照,且必须手动调用 runtime.Stack 或触发 GC 才可能拿到较全信息。它没法反映增长趋势,也不支持按需 HTTP 采集,线上诊断基本没用。真正可靠的是 pprof 的 HTTP handler:可定时抓取、可归档、可 diff —— 比如用 curl http://localhost:6060/debug/pprof/goroutine?debug=1 | wc -l 定时记录行数,做趋势比对。
如何用 go tool pprof 分析 debug=2 输出
把 /debug/pprof/goroutine?debug=2 的输出保存为 goroutines.txt,然后执行:go tool pprof -http=:8080 goroutines.txt。Web UI 启动后重点看:
- 「Top」页面找调用栈最深、出现频次最高的函数
- 在 search 框输入
yourpackage.*快速过滤业务代码 - 若发现几十个相同栈停在
select {}或ch := <-c,说明 channel 没被关闭或接收方已退出,发送方还在等 - 注意区分「活跃 goroutine」和「阻塞 goroutine」:后者卡在 channel、锁或 syscall 上,更难定位但风险更高
Go 1.24+ 新增的 goroutineleak 端点怎么用
旧方法靠人工比对,新版本有底层支持:
启动服务前加环境变量 GODEBUG=goleak=1,再访问 /debug/pprof/goroutineleak?debug=1。
首次访问可能为空,因为需要 GC 完成一次“泄漏感知循环”,建议先调用 runtime.GC() 再查。
返回堆栈中带 _Gleaked 标记的 goroutine 就是确定泄露项——但它只识别阻塞在不可达同步原语上的 goroutine,对无限重试、死循环这类主动不退出的场景无效。
最常被忽略的一点:pprof 显示 goroutine 很多但 heap 不涨,不代表没事。goroutine 本身开销小,但它可能持着大对象指针(比如闭包捕获了含 []byte 的 struct),导致内存被“钉住”无法回收。这时候得交叉看 /debug/pprof/goroutine?debug=2 和 /debug/pprof/heap,才能确认是不是 goroutine 在拖住内存。


















