runtime.NumGoroutine() 返回当前存活 goroutine 总数(含运行、就绪、阻塞等所有状态),是无锁瞬时快照,开销极低,适合每秒采样监控趋势,但不可用于精确追踪或替代 pprof 定位泄漏。

如何用 runtime.NumGoroutine() 实时获取当前 goroutine 数量
这是最直接、开销最小的方式,runtime.NumGoroutine() 返回当前运行时中所有 goroutine 的总数(包括正在运行、就绪、阻塞、休眠状态的)。它不区分用户创建还是系统内部 goroutine,但对监控趋势足够有效。
注意:该函数返回的是瞬时快照,不是原子计数器——在高并发场景下连续两次调用可能得到不同结果,但这不影响趋势观察。
- 适合每秒采样一次做基础监控,比如配合 Prometheus 暴露为
go_goroutines指标 - 不要在性能敏感热路径(如每毫秒调用)中频繁使用,虽快但仍有微小开销
- 不能用于精确追踪某段逻辑启停的 goroutine 数量,因为它不支持过滤或标签
为什么 pprof 里的 goroutine profile 不等于 runtime.NumGoroutine() 的值
runtime.NumGoroutine() 统计的是“存活 goroutine 总数”,而 pprof.Lookup("goroutine").WriteTo(...) 默认只抓取处于 非空闲状态 的 goroutine(即 stack trace 可获取的),默认模式是 "debug=1";若用 "debug=2" 则包含所有 goroutine(含 runtime 系统 goroutine 和已阻塞在 channel send/recv 上但无栈的)。
- 常见误判:看到 pprof 输出只有几十行,但
runtime.NumGoroutine()返回上千——大概率是大量 goroutine 阻塞在 channel 或 timer 上,未被debug=1捕获 - 调试泄漏时,务必用
curl http://localhost:6060/debug/pprof/goroutine?debug=2查看完整列表 - 程序启动后,即使没写
go关键字,runtime 也会维持若干后台 goroutine(如 sysmon、gc worker),数量通常在 3–10 个,属正常
如何安全地监控某段业务代码引发的 goroutine 泄漏
单纯看总数容易被干扰。更可靠的做法是“差分监控”:在关键入口/出口打点,记录 goroutine 数量变化,并结合上下文判断是否异常增长。
立即学习“go语言免费学习笔记(深入)”;
示例逻辑:
func trackGoroutines(label string, f func()) {
before := runtime.NumGoroutine()
defer func() {
after := runtime.NumGoroutine()
if after-before > 5 { // 允许少量波动
log.Printf("[leak-check] %s: +%d goroutines", label, after-before)
}
}()
f()
}
- 不要依赖绝对数值,重点看 delta 是否持续增大(尤其在循环或高频请求中)
- 避免在 defer 中直接调用
runtime.NumGoroutine()后立刻打印——需确保 f() 已完全退出,否则可能漏掉刚 spawn 但尚未调度的 goroutine - 若发现泄漏,下一步应立即采集
debug=2的 goroutine pprof,按 stack trace 聚合,定位重复出现的调用链
goroutine 数突增但 CPU/内存不高的常见原因
数量多 ≠ 负载高。很多 goroutine 处于休眠或阻塞态,不消耗 CPU,但会占用栈内存(默认 2KB,可收缩)和调度器元数据。
- 典型场景:大量 goroutine 阻塞在
time.Sleep()、net.Conn.Read()、未缓冲 channel 的 send/recv 上 - 一个
http.Server在高并发短连接下可能瞬间创建数千 goroutine,但多数很快结束——要看NumGoroutine()是否回落,而非峰值 - 真正危险的是“只增不减”的缓慢上涨,往往源于忘记 close channel、未处理的
context.Done()、或 timer 未Stop()
监控 goroutine 数量本身很简单,难的是把数字和真实行为对应起来。别只盯着数字涨了没,先确认它卡在哪、为什么没退、谁在 hold 它。


















