runtime.NumGoroutine() 返回当前已启动且尚未退出的所有 goroutine 总数,包括运行、就绪、阻塞及刚创建未调度的协程,涵盖用户与 runtime 内部协程,是无锁原子快照,开销极低。

runtime.NumGoroutine() 返回的是当前活跃 goroutine 数量
这个函数返回的是当前正在运行或处于可运行状态的 goroutine 总数,包括已启动但尚未结束、也未被调度器彻底清理的 goroutine。它不区分用户创建的还是系统后台启动的(比如 net/http 的监听协程、gc 相关协程等)。
常见误用是把它当作“业务 goroutine 计数器”来监控泄漏——但实际中,只要 http.Server 在跑、time.Ticker 活着、甚至 sync.Pool 内部用了 goroutine,数量就可能缓慢上涨,不一定代表泄漏。
- 调用开销极低,可高频采样(如每秒一次),但注意不要在 hot path 上频繁调用
- 结果是瞬时快照,goroutine 生命周期短时可能漏掉;想追踪增长趋势,建议搭配差值统计 + 日志打点
- 若发现数量持续上升且与业务并发量不匹配,优先检查是否忘了
closechannel、defer wg.Done()是否漏写、或select缺少default导致永久阻塞
runtime.ReadMemStats() 是获取内存使用最直接的方式
它会触发一次堆内存统计同步(不触发 GC),填充一个 runtime.MemStats 结构体。关键字段包括:
-
Alloc:当前已分配且仍在使用的字节数(即“活对象”内存,最接近你关心的“用了多少”) -
TotalAlloc:程序启动以来累计分配的字节数(含已被回收的,适合看吞吐) -
Sys:向操作系统申请的总内存(含堆、栈、全局变量、mcache/mspan 等运行时开销) -
HeapInuse:堆中已分配给对象的内存(≈Alloc,但不含未使用的 span) -
NumGC:GC 触发次数,配合PauseNs可分析 GC 频率和停顿影响
注意:ReadMemStats 是同步操作,高并发下频繁调用会影响性能;生产环境建议 5–30 秒采样一次,避免成为瓶颈。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
别直接用 runtime.GC() 强制触发 GC 来“压低 Alloc”
有人看到 Alloc 偏高就想手动调 runtime.GC(),这是典型误区。强制 GC:
- 会引发 STW(Stop-The-World),导致请求延迟毛刺,尤其在 QPS 高时更明显
- 无法解决根本问题——如果代码存在内存泄漏(比如不断往 map 存未清理的结构体),下次采集时
Alloc还会涨回来 - 干扰 Go 自身的 GC 决策节奏,可能让两代 GC 间隔异常,反而增加总体开销
真正该做的是:用 pprof 抓取 heap profile(curl http://localhost:6060/debug/pprof/heap?debug=1),结合 go tool pprof 分析哪些类型占内存最多、谁在持续 new 对象。
生产环境监控要避开 runtime 包的“假热点”
很多监控 agent 直接轮询 NumGoroutine() 和 ReadMemStats() 并上报,但容易忽略两点:
- goroutine 数量本身波动大:比如一个
http.HandlerFunc启动 10 个子 goroutine 处理分片,响应完就退出,但采样刚好卡在中间,数值跳变剧烈,误报“泄漏” - 内存指标需对齐 GC 周期:
Alloc在 GC 前后差异可达数倍,单独看某个值意义不大;应关注Alloc的 95% 分位趋势、或与NextGC的比值(如float64(ms.Alloc) / float64(ms.NextGC)> 0.8 就值得警惕)
真正稳定的信号,是长期(小时级)内 NumGC 持续升高 + PauseTotalNs 累计增长 + Alloc 底线缓慢上移——这三者同时发生,才大概率指向真实泄漏或负载失衡。

















