runtime.NumGoroutine 返回当前程序中所有活动 goroutine 的瞬时数量,包括运行、就绪、阻塞状态的协程,涵盖用户与 runtime 内部创建的协程,适合低开销水位监控但不可替代 profiling 定位具体问题。

runtime.NumGoroutine 返回的是什么值
runtime.NumGoroutine 返回当前程序中处于活动状态的 goroutine 数量,包括正在运行、就绪、阻塞(如在 channel 操作、系统调用、锁等待)中的 goroutine。它不区分用户启动还是 runtime 内部创建的 goroutine(比如 net/http 的监听协程、gc worker 等),所以数值通常比你显式 go f() 启动的要高。
这个值是瞬时快照,无锁读取,开销极低,适合高频采样;但注意它不提供 goroutine 的生命周期、堆栈或归属信息——想定位泄漏,得配合 pprof 或 debug.ReadGCStats 等工具。
什么时候该用 NumGoroutine 做负载预警
它最适合做粗粒度、低开销的“水位线”监控,比如判断服务是否持续堆积任务、连接池是否耗尽、或是否触发了异常并发模式。不适合替代 trace 或 profiling 定位具体瓶颈。
- HTTP 服务中,每秒采集一次
runtime.NumGoroutine,若连续 5 次 > 5000 且请求延迟上升,可触发告警 - 长周期任务调度器里,用它防止意外启动过多 goroutine 导致内存暴涨(例如忘记
selectdefault 分支导致无限go) - 和
runtime.MemStats中的NumGC对比:如果 goroutine 数飙升但 GC 次数平稳,大概率是协程未退出,而非内存泄漏本身
常见误用和踩坑点
直接拿 runtime.NumGoroutine() 当“活跃请求数”用是最大误区——HTTP handler 启动的 goroutine 可能早已返回,但 runtime 内部的 netpoll 协程、timer goroutine 仍计入总数;反过来,goroutine 数稳定也不代表负载低,可能所有协程都卡在 IO 等待上。
- 不要在循环里高频调用(如每毫秒),虽快但无意义:goroutine 创建/销毁本身有延迟,抖动大,反而掩盖趋势
- 避免仅靠单次阈值告警:突发流量会拉高数值,应结合滑动窗口(如过去 60 秒的 P95)或变化率(每秒增量 > 100)
- 容器环境下要注意:
runtime.NumGoroutine不受 cgroup CPU 或 memory 限制影响,但数值过高会导致调度器压力增大,加剧抢占延迟
一个轻量埋点示例
下面这段代码把 goroutine 数作为 Prometheus 指标暴露,每 5 秒采样一次,带简单差分逻辑过滤毛刺:
import (
"expvar"
"runtime"
"time"
)
func init() {
expvar.Publish("goroutines", expvar.Func(func() interface{} {
return runtime.NumGoroutine()
}))
}
// 在 HTTP handler 中注册 /debug/vars 即可访问
// 或用 Prometheus client 封装为 Gauge 类型指标
更实用的做法是搭配 expvar + net/http/pprof,既保留低侵入性,又能在出问题时快速拉取完整 goroutine stack(/debug/pprof/goroutine?debug=2)。
真正难的不是采集这个数字,而是理解它背后协程生命周期的完整性——比如一个 http.HandlerFunc 里启了个 goroutine 处理异步日志,却没做超时或 context 取消,它就会一直挂着,NumGoroutine 会默默上涨,直到 OOM。











