直接写http.ListenAndServe(":9090", promhttp.Handler())会编译失败,因promhttp.Handler()返回http.Handler接口,而http.ListenAndServe()第二参数需为nil或func(http.ResponseWriter, *http.Request)类型;正确做法是先http.Handle("/metrics", promhttp.Handler())再http.ListenAndServe(":9090", nil)。

http.ListenAndServe() 传 promhttp.Handler() 编译报错
直接写 http.ListenAndServe(":9090", promhttp.Handler()) 会编译失败,错误提示类似 cannot use promhttp.Handler() (value of type http.Handler) as type func(http.ResponseWriter, *http.Request) value in argument to http.ListenAndServe。
这是因为 http.ListenAndServe() 第二个参数接受两种类型:要么是 nil(表示用全局 http.DefaultServeMux),要么是符合 func(http.ResponseWriter, *http.Request) 签名的 handler 函数——而 promhttp.Handler() 返回的是 http.Handler 接口实例,不是函数。
- 正确写法一(推荐):
http.Handle("/metrics", promhttp.Handler()); http.ListenAndServe(":9090", nil) - 正确写法二(显式传 Handler):
http.ListenAndServe(":9090", promhttp.Handler())—— 这其实是合法的,因为http.ListenAndServe()内部会把http.Handler类型转为适配器,但前提是没提前往http.DefaultServeMux注册冲突路径 - 更稳妥的做法是新建独立 mux:
mux := http.NewServeMux(); mux.Handle("/metrics", promhttp.Handler()); http.ListenAndServe(":9090", mux)
自定义 CounterVec 打点时 panic: duplicate metrics collector registration
启动时 panic 报 duplicate metrics collector registration,基本等于你在多个地方调用了 prometheus.MustRegister() 同一个指标,或者把指标定义写在了 handler 函数里,每次请求都重新 New + Register。
根本原因:Prometheus 客户端对同名指标注册极其严格,不允许重复;且 prometheus.MustRegister() 是全局注册,一旦失败就 os.Exit(1),无法 recover。
- 所有指标变量必须声明为包级变量,例如:
var httpReqCounter = prometheus.NewCounterVec(...) - 注册动作只允许做一次,放在
init()或main()开头,例如:func init() { prometheus.MustRegister(httpReqCounter) } - 如果用了
promauto包(如promauto.NewCounterVec),它内部已自动注册,无需再手动调MustRegister() - 避免在 HTTP handler、goroutine 启动逻辑、或循环中调用
NewXXX()和MustRegister()
Gin/Echo 中 /metrics 返回 404 或空内容
明明写了路由,curl 也通,但 /metrics 返回 404 或空响应,常见原因是框架未真正挂载 Prometheus handler,或 registry 不匹配。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
标准库里 promhttp.Handler() 默认读取全局 registry;但 Gin/Echo 的中间件封装方式不同,容易漏掉关键适配步骤。
- Gin 正确写法:
router.GET("/metrics", gin.WrapH(promhttp.Handler())),别用c.String()或自己拼文本 - Echo 正确写法:
e.GET("/metrics", echo.WrapHandler(promhttp.Handler())) - 如果用了自定义 registry(比如测试隔离),必须用
promhttp.HandlerFor(reg, promhttp.HandlerOpts{}),并确保所有指标都注册到这个reg上 - 检查是否启用了 gzip 中间件或 JSON 格式化中间件——它们会干扰
text/plain; charset=utf-8响应头,导致 Prometheus 解析失败
活跃连接数一直涨不降,Gauge.Dec() 没被调用
用 prometheus.NewCounter() 统计“当前在线用户”或“WebSocket 连接数”,结果指标只增不减,监控曲线一路飙升——这是典型的指标类型误用。
Counter 只能加,不能减;而活跃连接是瞬时状态,goroutine 退出时必须显式扣减。否则指标永远不准,告警完全失效。
- 该用
prometheus.NewGauge(),不是 Counter - 每个新连接:调
connGauge.Inc() - 每个连接关闭前(务必在 defer 或明确退出路径里):调
connGauge.Dec() - 别依赖 GC 或 goroutine 自然结束——Gauge 不会自动清理,Dec 必须显式触发
- 如果连接管理分散在多处,建议封装成 struct,把 Inc/Dec 逻辑收口,避免遗漏
指标暴露本身不难,难的是注册时机、类型选择、标签控制和生命周期管理。最容易被忽略的是:**Gauge 的 Dec 必须和 Inc 成对出现,且发生在同一 goroutine 的可预测退出点**——这不是“最好这么做”,而是“不做就彻底失真”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










