runtime.numgoroutine() 返回当前程序中存活的 goroutine 总数,包括运行、就绪、阻塞及刚启动未调度的 goroutine,不区分用户或 runtime 创建,且为无锁快照值。

runtime.NumGoroutine() 返回的是什么数
runtime.NumGoroutine() 返回当前程序中**存活的 goroutine 总数**,包括正在运行、就绪、阻塞(比如在 channel 操作、syscall、time.Sleep)甚至刚启动还没调度的 goroutine。它不区分“用户创建”还是“runtime 内部使用”,也不过滤已退出但尚未被 GC 清理的——不过实际中这个延迟极短,可忽略。
常见错误现象:在 HTTP handler 里每请求调用一次 runtime.NumGoroutine(),发现数值持续上涨,误以为有 goroutine 泄漏;其实可能只是并发请求多、处理慢,goroutine 还没来得及退出。
- 它是个快照值,无锁读取,开销极低,适合高频采样
- 不能反映 goroutine 生命周期或阻塞原因,仅作数量参考
- 在测试中用它断言 goroutine 数量(比如 defer 后是否归零)时,要加小延时或用
runtime.Gosched()让调度器清理完,否则可能误判
什么时候该监控 goroutine 数量
监控 runtime.NumGoroutine() 本身不是目的,关键看场景:
- 服务上线后发现内存缓慢增长、GC 频率升高 → 查
runtime.NumGoroutine()是否单边上涨,再结合 pprof 查泄漏点 - HTTP 服务响应变慢,
runtime.NumGoroutine()稳定在几千以上 → 很可能有 channel 死锁、未关闭的http.Client连接池耗尽、或忘记cancel()的 context - 单元测试里验证 goroutine 泄漏:启动前记下
n1 := runtime.NumGoroutine(),执行逻辑后time.Sleep(10 * time.Millisecond)再查n2,若n2 > n1 + 2(+2 是测试框架自身开销)就值得深挖
别只看数字,要结合 pprof 定位真实问题
runtime.NumGoroutine() 告诉你“有多少”,但不说“在哪卡着”。数值异常高时,直接看 /debug/pprof/goroutine?debug=2 更有效:
立即学习“go语言免费学习笔记(深入)”;
-
?debug=1只显示 goroutine 数量摘要;?debug=2打印全部堆栈,能一眼看出哪些在select卡 channel、哪些在net.(*pollDesc).wait卡网络、哪些在sync.runtime_SemacquireMutex卡锁 - 注意区分 “running” 和 “IO wait” 状态——前者可能是 CPU 密集型 bug,后者更可能是资源未释放(如数据库连接、文件句柄)间接导致 goroutine 堵住
- 如果 pprof 显示大量 goroutine 停在
runtime.gopark且堆栈相似,大概率是某处 channel 写入没人读、或time.AfterFunc创建后没清理
生产环境监控的实用建议
在 Prometheus + Grafana 体系中暴露 goroutine 数量,比写日志更可靠:
- 用
prometheus.NewGaugeFunc注册一个指标,函数体直接返回runtime.NumGoroutine(),避免采样延迟 - 不要只设一个告警阈值(比如 >5000),要加同比/环比变化率:过去 5 分钟增长超 300% 才触发,避免误报突发流量
- 配合
runtime.ReadMemStats一起上报,如果NumGoroutine上涨同时Alloc也持续上涨,泄漏可能性极高;如果Alloc平稳,可能是临时并发高峰
最常被忽略的一点:很多团队把 runtime.NumGoroutine() 当成“健康度指标”加进大盘,却从不配置对应的 pprof 快速诊断入口——数字报警了,还得手动 curl 一次 pprof,等几十秒才能看到堆栈。这事得提前配好一键跳转链接,不然等于没监控。










