Go expvar.Publish 怎么暴露自定义运行指标
expvar.Publish 的作用,是把一个实现了 expvar.Var 的对象按名称放进进程级全局注册表。随后,标准处理器会把这些变量连同 cmdline、memstats 一起以 JSON 暴露出来。自定义运行指标最省事的写法通常不是手写 Var,而是用 expvar.Func 包装一个返回结构体或 map 的函数,再用 Publish 注册。
官方文档:https://pkg.go.dev/expvar
先明确接入范围:expvar 是进程内 JSON 指标表
我在给一个小型 HTTP 服务补运行状态时,最初准备自己维护一个 /status JSON 接口。真正动手后发现,计数器的并发更新、字段注册和 JSON 输出都在重复造轮子。迁移到 expvar 后,业务代码只负责更新值,注册表和输出由标准库处理。
expvar.Var 的契约很小:只有一个 String() string,但返回内容必须是有效 JSON。包内置的 Int、Float、String、Map 和 Func 都已经满足这一要求。内置变量的 Set、Add 等修改操作是原子的,适合直接放在并发请求路径里。

先选变量类型,避免把所有状态塞进一个 Func
| 类型 | 适合内容 | 更新方式 | 注意点 |
|---|---|---|---|
expvar.Int | 请求数、进行中请求数 | Add、Set | NewInt 创建时已经注册 |
expvar.Map | 按状态码或任务类型分组 | Add、Set | 键空间要有上限,避免高基数 |
expvar.Func | 读取时计算的组合快照 | 调用回调函数 | 回调要快、无副作用、不能 panic |
自定义 Var | 已有特殊状态对象 | 自行实现 | String 必须返回有效 JSON |
如果只是单个数值,优先使用 NewInt 或 NewFloat。如果需要把几个已经存在的状态组合成一次读取快照,才使用 Func。这样可以看出哪些值持续累积、哪些值在请求到达时临时计算。
用 Publish 注册自定义动态快照
下面把构建标识、就绪状态和累计请求数组合成一个 app_snapshot。expvar.Func 会在变量被读取时调用函数,并用 JSON 编码返回值;因此无需手工拼接 JSON 字符串。
package main
import (
"expvar"
"net/http"
)
var requestsTotal = expvar.NewInt("app_requests_total")
func init() {
// Publish 注册动态快照,名称在整个进程内必须唯一
expvar.Publish("app_snapshot", expvar.Func(func() any {
return struct {
Build string `json:"build"`
Ready bool `json:"ready"`
RequestsTotal int64 `json:"requests_total"`
}{
Build: "dev",
Ready: true,
RequestsTotal: requestsTotal.Value(),
}
}))
}
func hello(w http.ResponseWriter, r *http.Request) {
// 内置 Int 的 Add 是原子更新,可用于并发请求计数
requestsTotal.Add(1)
_, _ = w.Write([]byte("ok"))
}
这里有一个容易重复注册的点:expvar.NewInt("app_requests_total") 内部已经调用 Publish。不要再对同一个指针和同一个名称调用一次 Publish,否则重复名称会触发 log.Panic。给名称加应用前缀,也能减少不同包之间的碰撞。
把请求计数拆成总量、并发量和状态码
单个总数只能说明服务收到过多少请求。为了看出当前压力和结果分布,可以再增加一个进行中计数器与一个有界状态码 Map。状态码的取值集合有限,适合作为 Map 的键;用户 ID、完整 URL、错误文本则可能持续膨胀,不应该直接成为键。
package main
import (
"expvar"
"net/http"
"strconv"
)
var (
requestsInFlight = expvar.NewInt("app_requests_in_flight")
responsesByStatus = expvar.NewMap("app_responses_by_status")
)
type statusWriter struct {
http.ResponseWriter
status int
wroteHeader bool
}
func (w *statusWriter) WriteHeader(code int) {
// 只记录第一次状态码,保持与 net/http 的响应语义一致
if !w.wroteHeader {
w.status = code
w.wroteHeader = true
}
w.ResponseWriter.WriteHeader(code)
}
func withMetrics(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 进入时加一,退出时恢复当前并发量
requestsTotal.Add(1)
requestsInFlight.Add(1)
defer requestsInFlight.Add(-1)
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
responsesByStatus.Add(strconv.Itoa(sw.status), 1)
})
}
这段中间件只记录稳定维度。若业务需要更细的路径统计,应先把路由归一成固定模板,例如 /users/:id,不要直接使用带参数的原始 URL。
把 Handler 放到内部路由,再读取目标字段
导入 expvar 会在默认 ServeMux 注册 /debug/vars。如果项目使用自定义 mux,调用 expvar.Handler() 可以安装到非标准位置。下面使用 Go 1.22 及以上的带方法路由,并单独监听本机回环地址,避免把运行参数和内存统计直接暴露到公网。
func main() {
appMux := http.NewServeMux()
appMux.Handle("GET /hello", withMetrics(http.HandlerFunc(hello)))
debugMux := http.NewServeMux()
// Go 1.22+ 使用方法路由,并仅监听本机内部端口
debugMux.Handle("GET /debug/vars", expvar.Handler())
go func() {
_ = http.ListenAndServe("127.0.0.1:9090", debugMux)
}()
// 业务端口和调试端口分离,便于设置不同访问策略
_ = http.ListenAndServe(":8080", appMux)
}
Go 官方文档说明,从 Go 1.22 开始,默认的 /debug/vars 请求必须使用 GET。读取时可以只筛选关心的字段,避免终端一次输出完整的 memstats:
# 读取内部端点,并只保留本文注册的三个指标
curl -fsS http://127.0.0.1:9090/debug/vars \
| jq '{app_requests_total, app_requests_in_flight, app_responses_by_status, app_snapshot}'

回归检查:迁移后重点看六个风险
- 名称唯一:同一个进程里的所有发布变量共用注册表,重复名称会直接 panic。
- JSON 有效:自定义
Var.String必须返回合法 JSON;优先用Func让标准库编码。 - 回调足够快:
Func在读取指标时执行,不要在里面访问慢数据库、发网络请求或修改业务状态。 - 键空间有限:
Map不会自动替你限制标签数量,避免把用户输入当键。 - 端点不公开:默认内容包含命令行参数和内存统计,应使用内部监听、反向代理鉴权或网络访问控制。
- 采集格式匹配:
expvar输出 JSON,不是 Prometheus 文本格式;接入现有采集系统前先确认适配方式。
常见问题
Publish 必须写在 init 里吗?
文档建议在包创建变量时从 init 调用,便于启动阶段完成固定注册。技术上可在其他位置调用,但并发动态注册会增加生命周期和重名判断的复杂度。
为什么已经 NewInt 还会重名 panic?
NewInt、NewMap 等构造函数会自动发布变量。通常是多个包用了同一个名称,或代码又手动调用了一次 Publish。
expvar 能替代完整监控系统吗?
它适合轻量进程状态、调试端点和 JSON 采集源,但不提供历史存储、告警、查询语言或仪表盘。生产监控仍要由采集与存储系统承接。
迁移清单
把自制状态接口迁移到 expvar 时,先把指标分成原子数值、有限分类和动态快照;分别使用 Int、Map、Func;确认名称唯一与 JSON 合法;再把 Handler 放进受限的内部路由。完成后,用一次 GET 只读取目标字段,并检查计数增长、并发量归零、状态码键集合稳定。这样 Publish 才真正减少维护成本,而不是把一个自制接口换成另一个不可控入口。
Go String 方法里调用 fmt 为什么可能无限递归
- 上一篇
- Go String 方法里调用 fmt 为什么可能无限递归
- 下一篇
- Git worktree 怎么清理失效工作区记录
-
- Golang · Go教程 | 32分钟前 | 源码分析 · Go教程 · Go go/parser go/ast 语法树遍历 ast.Inspect token.FileSet
- Go ast.Inspect 怎么查找指定语法节点
- 197浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go errors.AsType 怎么从错误链提取具体类型
- 127浏览 收藏
-
- Golang · Go教程 | 2小时前 | 标准库 · 流式处理 · Go教程 · Go token 流式解析 encoding/xml xml.Decoder 大型XML
- Go xml.Decoder.Token 怎么流式处理大型 XML
- 326浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go pem.Decode 怎么连续读取多个 PEM 区块
- 344浏览 收藏
-
- Golang · Go教程 | 2小时前 | go · Go encoding/json json.Decoder InputOffset
- Go json.Decoder.InputOffset 怎么定位解析错误附近字节
- 298浏览 收藏
-
- Golang · Go教程 | 3小时前 | go · Go io.Writer encoding/hex hex.Dumper
- Go hex.Dumper 怎么流式输出可读十六进制内容
- 159浏览 收藏
-
- Golang · Go教程 | 3小时前 | 切片 · go · Go encoding/binary binary.AppendUvarint varint
- Go binary.AppendUvarint 怎么追加变长整数
- 156浏览 收藏
-
- Golang · Go教程 | 4小时前 | 标准库 · 性能优化 · Go教程 · base64 Go encoding/base64 切片复用 AppendEncode EncodedLen
- Go base64.Encoding.AppendEncode 怎么复用目标缓冲区
- 285浏览 收藏
-
- Golang · Go教程 | 4小时前 |
- Go base32.Encoding.WithPadding 怎么生成无填充编码
- 473浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 326次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 385次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 376次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 343次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 170次使用
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- 快速掌握Go语言HTTP标准库的实现方法
- 2022-12-30 327浏览
-
- HTTP服务压力测试工具及相关术语讲解
- 2023-01-07 485浏览

