expvar 默认不监听端口,因其仅注册变量到 http.DefaultServeMux 而不启动 HTTP server;需显式调用 http.ListenAndServe 并注意安全暴露与重复注册问题。

expvar 为什么默认监听 localhost 且不暴露端口
因为 expvar 本身不启动 HTTP server,它只是注册了一组变量到 http.DefaultServeMux 的 /debug/vars 路径下。没手动调用 http.ListenAndServe,就根本不会监听任何端口——你看到的“页面打不开”,大概率是服务压根没跑 HTTP 服务,或者 mux 被覆盖了。
- 必须显式启动 HTTP server,比如
http.ListenAndServe(":6060", nil) - 如果用了自定义
http.ServeMux(如 Gin、Echo),expvar不会自动挂载过去,得手动mux.Handle("/debug/vars", expvar.Handler()) - 默认只绑定
localhost是 Go HTTP server 的行为,不是expvar限制;想外网访问,得改监听地址为":6060"或"0.0.0.0:6060",但要注意防火墙和安全策略
怎么安全地暴露 /debug/vars 页面
直接把 /debug/vars 暴露到公网等于交出进程内存快照——所有注册的 expvar.NewInt、expvar.NewFloat、甚至 runtime.ReadMemStats 数据全在响应里。生产环境必须加访问控制。
- 用中间件拦截:在
http.HandleFunc前检查r.RemoteAddr或加 Basic Auth(注意别硬编码密码) - 换路径名:避免扫描器自动命中,比如注册到
/health/metrics而非默认路径 - 禁用敏感字段:
expvar.Do可遍历变量,自行过滤掉含password、token等关键字的Var名称 - 注意:Go 1.21+ 默认禁用
expvar中的goroutines堆栈(太重),但memstats仍包含分配峰值等关键信息
自定义指标时 panic: "already registered" 怎么办
expvar.Publish 和 expvar.NewXXX 都是全局注册,重复调用同名变量会直接 panic,常见于热重载、测试多轮初始化、或 init 函数里反复执行。
- 先尝试
expvar.Get("my_counter"),若返回非 nil 再跳过新建 - 不要在 HTTP handler 里调用
NewInt——每次请求都注册一次必炸 - 更稳妥的做法:所有指标在
main.init或main.main开头一次性声明,用包级变量持有引用 - 注意
expvar.Map的子项也受此约束,map.Set("key", new(expvar.Int))中的new(expvar.Int)没问题,但map.Set("key", expvar.NewInt())若重复调用就会冲突
JSON 响应里看到大量 goroutine 数但不知道谁在涨
/debug/vars 返回的 goroutines 是个数字,只能告诉你“现在有 127 个”,没法定位泄漏点。它和 /debug/pprof/goroutine?debug=2 完全不是一回事。
立即学习“go语言免费学习笔记(深入)”;
-
expvar的goroutines来自runtime.NumGoroutine(),纯计数,无堆栈 - 真要查泄漏,必须用 pprof:启动时加
_ "net/http/pprof",然后访问/debug/pprof/goroutine?debug=2 - 可以写个简易巡检脚本,定时 GET
/debug/vars解析goroutines字段,连续三次增长超 20% 就告警——但告警后必须切 pprof 查根因 - 注意:某些库(如旧版 sqlx、部分 grpc-go 版本)会在连接池空闲时残留 goroutine,不是代码 bug,而是预期行为
真正麻烦的是混用 expvar 和结构化日志——比如把 expvar.String 当配置中心用,结果发现更新不生效,其实是字符串值被序列化成 JSON 后丢了引用关系。这种隐性耦合,文档里从不提,但线上排障时最耗时间。










