runtime.NumGoroutine()返回当前已启动且尚未退出的goroutine总数,包括运行、就绪、阻塞及刚创建未调度的协程,是无锁原子快照,开销极低;它包含用户与runtime内部协程,不能区分来源,仅作数量参考。
直接调用
就行
它返回的是当前已启动且尚未退出的 goroutine 总数,包括正在运行、就绪、阻塞(如 channel 等待、syscall、time.Sleep)、甚至刚创建还没调度的协程。这是个无锁原子读取,开销极低,调用即返回,完全不影响程序运行。
常见误判是看到数值为 7 就以为自己写了 5 个 goroutine 没关——其实其中 2–5 个大概率是 runtime 自带的 netpoll worker、timer goroutine 或 GC 辅助协程,它们长期存在且稳定。
值为
表示只有
goroutine 在跑,程序基本空闲
值持续 >1000 不一定异常,但若伴随 CPU/内存上涨,就得查泄漏
别在每请求 handler 里狂打
,日志 IO 才是瓶颈,不是这个函数
为什么
和
数值不一致?
是一个整数快照;而
输出的是所有 goroutine 的栈跟踪列表,默认会过滤掉大量 idle 状态的 runtime 内部协程(比如
在 netpoll 上休眠的那些)。
所以你看到的 pprof 行数通常比
小,差值就是这些被隐藏的 idle worker。想尽量对齐,得用
,但即使如此,某些刚创建还没入调度队列的 goroutine 仍可能不出现。
pprof 的
只显示“正在运行或可运行”的 goroutine,摘要式统计
才打印全部堆栈,适合人工排查阻塞点
数值差异本身不是 bug,是视角不同:一个是总数,一个是可观察堆栈快照
想监控特定业务逻辑的 goroutine 数量怎么办?
无法区分用户代码和 runtime 协程,更没法按函数维度拆解。真要精确统计某类任务(比如
或
)的并发实例数,必须自己埋点。
用
做原子计数是最轻量、最可靠的方式:入口
,出口用
,读取时用
。
计数器变量必须是包级全局
,不能是局部变量
务必用
,否则 panic 会导致计数永远卡住
不要用
包裹,原子操作足够,加锁反而引入竞争
生产环境暴露指标时最容易踩的坑
把
直接喂给 Prometheus,不能图省事写成
—— 这会让每次 HTTP 抓取
都触发一次实时计算,可能卡住 metrics endpoint。
正确做法是启一个后台 goroutine,每 1–5 秒调用一次
,再注册该 gauge 实例。
更新间隔别小于 1 秒:runtime 内部有读写锁,太高频反而增加争用
别漏掉
,否则 /metrics 返回 200 但没数据
如果做告警,建议用滑动窗口最大值(如最近 30 秒),避免瞬时 burst 导致误报
实际泄漏往往藏在第三方库启动却没提供关闭接口的 goroutine 里,比如某些 logger、metrics collector 或旧版 http client。数值只是线索,
才是定位卡点的第一现场。
runtime.NumGoroutine()1mainlog.Println(runtime.NumGoroutine())pprof/goroutine?debug=2runtime.NumGoroutine()runtime.NumGoroutine()/debug/pprof/goroutine?debug=2runtime.goparkruntime.NumGoroutine()?debug=2?debug=1?debug=2runtime.NumGoroutine()processOrderfetchFromDBsync/atomicatomic.AddInt64(&counter, 1)defer atomic.AddInt64(&counter, -1)atomic.LoadInt64(&counter)int64defersync.Mutexruntime.NumGoroutine()prometheus.NewGaugeFunc(..., func() float64 { return float64(runtime.NumGoroutine()) })/metricsgauge.Set(float64(runtime.NumGoroutine()))prometheus.MustRegister(gauge)/debug/pprof/goroutine?debug=2