直接调用 runtime.NumGoroutine() 可实时获取当前活跃 goroutine 总数,包含用户及运行时内部 goroutine,开销低但需注意其非纯用户视角;用于监控时须封装为 Prometheus Gauge 并定期更新,避免在 metrics handler 中动态计算;定位问题应结合 pprof/goroutine 堆栈分析而非仅看数值。

怎么实时获取当前 Goroutine 数量
Go 运行时暴露了 runtime.NumGoroutine(),它返回的是当前活跃的 goroutine 总数(包括正在运行、就绪、阻塞中的)。这不是采样值,是原子读取,开销极低,适合高频调用。
常见错误是把它当成“用户创建的 goroutine 数”,其实它包含 runtime 内部使用的(比如 netpoll、timer goroutine),数量通常在 2–5 个左右,稳定但非零。别一看到 7 就以为自己漏了 5 个没关。
- 直接调用
runtime.NumGoroutine()即可,无需 import 额外包 - 不要在 hot path(如每请求都打点)里反复调用并打印——虽然函数快,但日志 IO 会拖慢整体
- 如果做阈值告警,建议用滑动窗口统计(比如最近 30 秒最大值),避免瞬时毛刺误报
把 Goroutine 数喂给 Prometheus
Prometheus 是最常用的 Go 指标采集目标,runtime.NumGoroutine() 必须包装成 prometheus.Gauge 才能被正确抓取。不能直接注册一个函数,因为 prometheus 的 Collect() 要求指标值在收集时刻确定。
典型错误是写成闭包式注册:prometheus.NewGaugeFunc(..., func() float64 { return float64(runtime.NumGoroutine()) }) —— 看似简洁,但会导致指标在 scrape 时才计算,而 scrape 是 HTTP handler 触发的,可能卡住 metrics endpoint。
立即学习“go语言免费学习笔记(深入)”;
- 用
prometheus.NewGauge()创建 gauge 实例,再在后台 goroutine 里定期更新:gauge.Set(float64(runtime.NumGoroutine())) - 更新间隔建议 1–5 秒;太短增加锁竞争(
runtime内部有读写锁),太长失去监控意义 - 务必调用
prometheus.MustRegister(gauge),否则 /metrics 返回 200 但不包含该指标
可视化时 goroutine 曲线突然飙升却查不到源头
曲线飙高只说明数量突增,不代表泄漏——可能是合法的并发压测、批量任务启动。真正要定位问题,得结合堆栈和生命周期分析。
容易踩的坑:只看数字,不看 debug.ReadStacks() 或 pprof/goroutine。很多“泄漏”其实是 goroutine 卡在 channel receive、time.Sleep 或 net.Conn.Read 上,它们仍算活跃,但已无业务进展。
- 用
curl http://localhost:6060/debug/pprof/goroutine?debug=2查看完整堆栈(需启用net/http/pprof) - 重点关注状态为
IO wait、chan receive、select且持续超 10 秒的 goroutine - 如果用
pprofweb 界面,选 “goroutines” 类型后点 “Top” 再按 `flat` 排序,比默认的 `cum` 更易发现堆积点
用 Grafana 看 goroutine 指标要注意的兼容细节
Grafana 查询 Prometheus 时,默认用的是 rate() 函数,但 goroutines 是绝对值指标,不是计数器。对 gauge 类型用 rate() 会得到毫无意义的斜率(单位是 goroutine/秒),而且数值常为负。
另一个坑是时间范围选择:如果用 Last 5m,但采集间隔设成 10 秒,Grafana 默认只取最近几个点插值,可能错过峰值。
- 查询语句必须用原始值或
max_over_time(goroutines[5m])这类聚合,别碰rate()或increase() - 在 Grafana panel 设置里,把 “Min interval” 设为略大于你的采集间隔(比如采集是 3s,这里填 4s),避免采样丢失
- 加一条告警规则:当
max_over_time(goroutines[1m]) > 500持续 3 个周期,才触发——防止单次抖动误报
goroutine 数本身不泄露内存,但它是并发健康度最敏感的体温计。真正难的不是采集,是区分“合理增长”和“隐性堆积”——后者往往藏在 select 分支缺失、channel 未关闭、context.Done() 忽略这些地方。










