pprof 默认暴露 goroutine profile,访问 /debug/pprof/goroutines?debug=2 首行即为当前协程总数,适合监控;debug=1 返回完整栈迹用于排查阻塞,需确保 pprof 正确注册且生产环境限制访问。

pprof 默认不暴露协程数量,得手动注册 goroutine profile
Go 的 net/http/pprof 默认只启用 goroutine、heap、cpu 等几个 profile,但其中 goroutine profile 是开启的——它返回当前所有 goroutine 的栈迹,默认以 text 形式呈现。不过很多人误以为它“不显示数量”,其实是没解析或没注意响应体第一行:goroutine profile: total 1234 就是当前活跃协程数。
关键点在于:这个 profile 必须通过 HTTP 访问(如 /debug/pprof/goroutines?debug=1),且默认仅在 net/http/pprof 被导入并注册后才可用。如果用的是自定义 HTTP server 或非标准 mux,得手动调用 pprof.Register 并确保 handler 正确挂载。
- 没导入
_ "net/http/pprof"→/debug/pprof/路径 404 - 用了
http.ServeMux但没调用pprof.Handler("goroutine").ServeHTTP→ profile 不生效 - 启用了
GODEBUG=gctrace=1之类调试变量,会干扰 goroutine 统计准确性(尤其短生命周期协程)
用 debug=2 参数获取 goroutine 数量摘要,避免解析全文
/debug/pprof/goroutines?debug=1 返回完整栈迹,体积大、解析慢;而 ?debug=2 只返回按状态分组的统计摘要,首行就是总数,后续是 running、syscall、wait 等状态的 goroutine 数量,适合监控脚本快速提取。
例如 curl 请求后直接用 head -n1 就能拿到总数:
curl -s 'http://localhost:6060/debug/pprof/goroutines?debug=2' | head -n1 # 输出:goroutine profile: total 87
-
debug=1:返回全部 goroutine 栈,适合人工排查阻塞点 -
debug=2:返回聚合统计,适合 Prometheus exporter 或健康检查轮询 - 注意:该 endpoint 没有认证,生产环境务必限制访问 IP 或加反向代理鉴权
用 runtime.NumGoroutine() 实时读取,但无法替代 pprof 的深度诊断
runtime.NumGoroutine() 是最轻量的获取当前协程数的方式,开销极低,适合嵌入业务指标打点(比如每 10 秒上报一次)。但它只返回一个整数,没有任何上下文——不知道哪些 goroutine 在跑、是否泄漏、卡在哪一行。
典型误用场景:
- 只依赖
NumGoroutine()做告警,却没配pprof,发现飙升后无法快速定位源头 - 在高并发下频繁调用
NumGoroutine()(比如每毫秒)→ 无必要,该函数本身是原子读,但高频打点会拖慢 metrics 采集 - 把该值和
pprof/goroutines返回数对比,发现不一致 → 正常,因为两者不是同一时刻快照,且NumGoroutine()不包含正在创建/销毁中的临时 goroutine
协程泄漏排查时,别只盯总数,重点看 goroutine 状态分布
协程数缓慢上涨不一定代表泄漏,但若 debug=2 输出中 chan receive 或 select 状态长期占多数,大概率存在 channel 未关闭、select 缺少 default 分支、或 WaitGroup 使用错误。
实操建议:
- 定期抓取
/debug/pprof/goroutines?debug=2,记录各状态变化趋势(比如IO wait持续增长可能表示 net.Conn 泄漏) - 对比
debug=1输出里重复出现的栈帧,特别是涉及http.HandlerFunc、time.AfterFunc、go func() {...}()的闭包调用 - 用
go tool pprof http://localhost:6060/debug/pprof/goroutines进入交互模式,执行top查看高频栈,比肉眼扫更快
真正难的不是看到数字变大,而是判断哪个 goroutine 本该结束却一直活着——这需要结合代码路径、channel 生命周期和超时控制来交叉验证。











