runtime.numgoroutine()返回当前所有存活goroutine总数,含runtime内部协程(如netpoll、timerproc、sysmon等),非仅用户创建或正在cpu执行的协程,空程序通常为2–5。

直接调用 runtime.NumGoroutine() 就能拿到当前活跃 goroutine 总数,但它返回的是包括 runtime 内部协程在内的全部存活协程,不是“正在 CPU 上执行”的数量——Go 调度器不暴露真实运行中(runnable on M)的精确计数。
为什么 runtime.NumGoroutine() 返回值比你预期的多
空程序通常返回 2–5,这是因为 Go 运行时自带常驻协程:
-
netpollworker:负责网络 I/O 多路复用 -
timerproc:管理time.After、time.Ticker等定时任务 -
sysmon:系统监控协程,检查长时间阻塞、抢占等 - HTTP server 的 listener 协程(如
http.Server.Serve)也会被计入
所以看到数值是 8,不代表你写了 7 个 goroutine 没关——可能其中 4 个是 runtime 自带的。别一惊一乍,先看基线。
如何在 Prometheus 中安全暴露 goroutine 数量
不能把 runtime.NumGoroutine() 直接塞进 prometheus.NewGaugeFunc(),否则每次 scrape 都会触发一次读取,可能卡住 metrics endpoint。正确做法是:
- 用
prometheus.NewGauge()创建一个 gauge 实例 - 起一个后台 goroutine,每 2–5 秒调用一次
gauge.Set(float64(runtime.NumGoroutine())) - 必须调用
prometheus.MustRegister(gauge),否则指标不会出现在/metrics响应里 - 避免在 HTTP handler 里动态计算并返回,尤其当 scrape 间隔 ≤1s 时,会加剧 runtime 内部锁竞争
怎么判断是不是真的泄漏,而不是正常波动
单看数字上涨没意义,关键看趋势和回落行为:
- 随请求量线性增长,且请求结束后能回落 → 正常并发模型
- 数字持续单向爬升,哪怕无流量也缓慢上涨 → 泄漏嫌疑大
- 突增后不回落,但
/debug/pprof/goroutine?debug=1显示大量重复路径(如workerLoop、http.(*conn).serve)→ 定位到具体函数 - runtime 内部协程数量也在涨(比如 timerproc 从 1 变成 5)→ 可能有 listener 或 ticker 忘记
Stop()
真正难缠的泄漏往往不体现在总数上,而是 goroutine 卡在 chan send、semacquire 或未关闭的 http.Client 连接上——这时 runtime.NumGoroutine() 只是第一个报警信号,必须立刻抓 /debug/pprof/goroutine?debug=2 看堆栈。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











