用 promhttp 暴露 Go 运行时指标需显式创建 Registry 并注册 NewGoCollector 和 NewProcessCollector,再通过 HandlerFor 绑定;切勿使用默认 Handler,避免指标缺失、命名冲突或空响应。

如何用 promhttp 暴露 Go 程序的运行时指标
Go 标准库自带 runtime 和 debug 包,能直接拿到 GC、goroutine 数、内存分配等核心指标,但它们只是“可读”,不满足 Prometheus 的拉取协议。必须通过 promhttp 把这些数据转成符合 /metrics 接口规范的文本格式。
实操上别自己拼字符串——用 prometheus.NewRegistry() 注册器 + prometheus.MustRegister() 绑定标准指标,再用 promhttp.HandlerFor() 暴露:
import (
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
<p>func main() {
reg := prometheus.NewRegistry()
// 注册 Go 运行时指标(自动采集)
reg.MustRegister(prometheus.NewGoCollector())
reg.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}))</p><pre class='brush:php;toolbar:false;'>http.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
http.ListenAndServe(":9090", nil)}
常见错误:直接用 promhttp.Handler()(它用默认注册器),结果 runtime 指标没被注册进去,/metrics 里看不到 go_goroutines 或 go_memstats_alloc_bytes —— 一定显式传入自定义 reg。
立即学习“go语言免费学习笔记(深入)”;
为什么 NewGoCollector() 不采集 runtime.ReadMemStats 全量字段
NewGoCollector() 只暴露了最常用、开销可控的子集,比如 go_gc_duration_seconds、go_goroutines、go_memstats_*_bytes。像 MemStats.PauseNs 或 NumGC 这类字段,默认不导出,因为高频采集会影响性能,且 Prometheus 采样间隔通常在秒级,没必要。
如果真需要,得手动封装:
- 用
runtime.ReadMemStats(&ms)定期读取,转成prometheus.GaugeVec或prometheus.Counter - 注意加锁或用
sync/atomic,避免并发读写MemStats结构体 - 别在每次 HTTP 请求里调用
ReadMemStats—— 它会 STW(Stop-The-World)一小段时间,压测时可能拖慢/metrics响应
自定义指标命名冲突:避免和 go_*、process_* 前缀撞车
Prometheus 官方约定:go_* 归 Go Collector,process_* 归 Process Collector。如果你定义 go_requests_total,不仅语义错乱,还可能被后续升级的 client_golang 自动覆盖或报重复注册错误。
正确做法:
- 用业务前缀,比如
myapp_http_requests_total、myapp_cache_hits - 所有自定义指标必须用
prometheus.NewCounterVec等构造,**显式调用reg.MustRegister()**,别依赖全局注册器 - 检查是否重复:启动时加
log.Printf("registered metrics: %+v", reg.Gather()),看有没有 duplicate metric name 错误
Exporter 启动后 /metrics 返回空或 404
不是代码问题,大概率是路由注册顺序或路径写错。Go 的 http.ServeMux 是精确匹配,http.Handle("/metrics", ...) 不会响应 /metrics/(带尾部斜杠)。
容易踩的坑:
- 用了
http.HandleFunc但 handler 函数里忘了写w.WriteHeader(200)或w.Write([]byte(...)),导致空响应 - 把
http.Handle写在http.ListenAndServe之后 —— Go 不报错,但路由根本没生效 - 在 Gin/echo 等框架里混用
net/http原生 handler,没正确挂载到框架路由下,比如 Gin 要用gin.WrapH(promhttp.HandlerFor(...))
最稳的验证方式:启动后 curl -v http://localhost:9090/metrics,看 status code 和 body 是否含 # TYPE go_goroutines gauge 行。
复杂点在于 runtime 指标本身是延迟初始化的——第一次 GC 或 goroutine 创建后才出现对应指标,刚启动时看到部分指标为空是正常的。别急着改代码,先触发点负载试试。










