/expvar 不是开箱即用的监控系统,它需显式注册 handler 且变量必须正确创建并保持全局引用;/debug/vars 返回 404 或空 JSON 是因 expvar 本身不启动 HTTP 服务,也未自动挂载 handler,常见错误包括未调用 http.Handle、使用自定义 ServeMux 未手动挂载、路径大小写或前缀不匹配、pprof 冲突导致重复注册 panic,以及变量在函数内定义被 GC 回收。

expvar 不是开箱即用的监控系统,它只在你显式注册 handler 且变量被正确创建并保持引用时才生效;默认路径 /debug/vars 看似自动,实则极易因路由冲突、变量未注册或 GC 回收而返回空或 404。
为什么 curl /debug/vars 返回 404 或空 JSON
expvar 包本身不启动 HTTP 服务,也不自动挂载 handler——它只是提供了 expvar.Handler() 这个函数。常见错误包括:
- 没调用
http.Handle("/debug/vars", expvar.Handler()),或用了自定义http.ServeMux却忘了手动挂载 - 同时启用了
net/http/pprof,又重复注册/debug/vars,触发 panic:http: multiple registrations for /debug/vars - 用 Gin/Echo/Chi 等框架时,默认 mux 被覆盖,
expvar.Handler()完全不生效 - 路径写错:大小写敏感(
/debug/VARS)、带尾斜杠(/debug/vars/)或加了前缀(/api/debug/vars)都不匹配
验证方式:启动服务后直接运行 curl -v http://localhost:8080/debug/vars,看响应状态码和 Content-Type: text/plain; charset=utf-8 是否存在。
expvar.NewInt 创建后访问不到指标值
变量创建后“消失”,本质是未被全局注册表收录或被 GC 回收。关键点:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
-
expvar.NewInt("hits")内部已调用expvar.Publish,但仅当变量名不重复且变量对象未被丢弃时才有效 - 若定义在函数内(如
func init() { expvar.NewInt("hits") }),该变量无全局引用,可能被 GC 清理 - 变量名含非法字符(如点号
"api.latency"、空格、斜杠)会导致注册失败,且无报错 - 用
expvar.Get("hits")检查是否存在更可靠:返回nil表示未注册或已被移除
推荐写法:
var hits = expvar.NewInt("http_requests_total")
func handler(w http.ResponseWriter, r *http.Request) {
hits.Add(1)
}确保 hits 是包级变量,保持强引用。
往 expvar.Map 里塞结构体或指针为什么会 panic
expvar 序列化依赖 json.Marshal,但它只接受有限类型:基本类型(int/float64/string)、map[string]interface{}、[]interface{} 及其嵌套组合。以下操作都会 panic:
-
stats.Set("db_conn", &sql.DB{})—— 存指针,json无法序列化 -
stats.Set("last_update", time.Now())——time.Time不在白名单 -
stats.Set("config", Config{Port: 8080})—— 自定义 struct 默认无导出字段或MarshalJSON方法
安全做法:
stats := expvar.NewMap("db")
stats.Set("idle", expvar.NewInt("idle_conns"))
stats.Set("inuse", expvar.NewInt("inuse_conns"))
// 时间戳转 int64
stats.Set("updated_at", expvar.NewInt("updated_at_unix"))
stats.Get("updated_at").(*expvar.Int).Set(time.Now().Unix())
生产环境暴露 /debug/vars 的真实风险
它没有认证、无 TLS、无速率限制、不支持路径重写,且默认暴露 cmdline(含启动参数)、memstats(含内存布局)、Goroutines(可推断并发模型)。更隐蔽的问题:
-
pprof和expvar共存时,/debug/vars响应会变慢——因为要遍历所有 runtime 注册项,尤其是memstats这种大对象 - Go 1.21+ 把数字转为字符串输出(如
"heap_alloc":"12345678"),前端解析逻辑若仍按 number 处理会失败 - 无法加 label,所有维度靠变量名硬编码(
http_requests_get/http_requests_post),业务扩展后命名爆炸 - 不能替代 Prometheus:缺少类型声明、时间戳、采样控制,
expvar_exporter只是尽力而为的胶水层
上线前务必做两件事:改路径(如 /internal/metrics),加中间件鉴权(IP 白名单或 Bearer Token);别把 expvar 当生产监控主力,它只适合快速诊断和内部可观测性补位。

















