Go expvar 怎样暴露自定义指标映射
如果服务里有一组按接口、租户或缓存类型拆分的统计值,直接注册几十个顶层变量很快就会变得难以维护。Go 的 expvar.Map 可以把这些值收拢成一个字符串到指标变量的映射,再用 expvar.Publish 以一个稳定名称对外暴露。这样监控端只需要记住一个指标组名,业务代码也能继续按键更新。
官方文档:https://pkg.go.dev/expvar
核心做法是先创建一个命名 Map,再用Publish把它注册到 expvar 的全局变量表。计数型值优先使用Add,需要替换的值使用Set;同一个顶层名称重复发布会触发 panic,因此注册动作应集中在初始化阶段。
先把“指标组名”和“业务键”分开
Publish 管理的是顶层名称,Map 管理的是这个名称下面的键。比如把顶层名称定为 service_metrics,键再分成 api、cache 和 queue。这种层次比把 api_requests、cache_hits 等全部注册到全局表更容易扩展。

命名时可以先回答两个问题:这个 Map 是否表达同一类统计,以及监控方是否会把它作为一个对象整体读取。若答案都是“是”,就把维度放进 Map;若两个值的生命周期和含义完全不同,再考虑拆成独立变量。
创建 Map:计数和替换使用不同方法
expvar.Map 实现了 expvar.Var,内部保存字符串键到指标变量的关系。Map 中的值也必须是 expvar 变量,例如 Int、Float 或自定义的 Var。下面先用零值 Map 演示显式注册,避免把创建和注册混成两个职责。
package metrics
import "expvar"
var serviceMetrics = new(expvar.Map)
func recordRequest(kind string) {
// Add 适合累计同一业务键的请求次数;Map 会维护键到 Int 的关系。
serviceMetrics.Add(kind+"_requests", 1)
}
func setQueueDepth(depth int64) {
// Set 用于替换当前值,传入的变量需要实现 expvar.Var。
value := new(expvar.Int)
value.Set(depth)
serviceMetrics.Set("queue_depth", value)
}
func currentRequestCount(kind string) int64 {
// Get 只读取已有键;未注册的键可能返回 nil,调用前要判断。
v := serviceMetrics.Get(kind + "_requests")
if v == nil {
return 0
}
return v.(*expvar.Int).Value()
}
这里有一个容易混淆的点:Add 针对的是 Map 中已经存在或将要创建的 *Int 值,而 Set 可以放入任意实现 expvar.Var 的值。对于请求量、命中量这类只增不减的统计,使用 Add 更直观;对于队列深度、当前连接数这类快照值,用 Set 表达“替换当前观测值”更合适。
如果业务键来自外部输入,不要直接把未经约束的长字符串当作键。键的数量无限增长会让指标输出变大,也会让监控标签失去稳定性。可以在业务边界把输入归并到有限集合,例如 read、write 和 other。
用 Publish 把 Map 放进 expvar 全局表
仅仅创建一个 Map,并不等于它已经出现在 expvar 的公开变量集合里。若希望通过 expvar 的标准处理器读取,就需要为 Map 选择一个全局唯一名称并发布。
package metrics
import "expvar"
var serviceMetrics = new(expvar.Map)
func init() {
// Init 准备零值 Map,再由 Publish 完成一次全局注册。
serviceMetrics.Init()
// 名称重复时会 panic,因此注册点应保持唯一。
expvar.Publish("service_metrics", serviceMetrics)
}
func recordCacheHit() {
// 业务代码只关心 Map 的键,不需要重复注册顶层变量。
serviceMetrics.Add("cache_hits", 1)
}
更常见的组织方式是只保留一份 Map,并在包初始化阶段完成一次注册。不要在每次请求、每次重载配置或每个测试用例里反复调用 Publish;全局表中已有同名变量时,重复发布不是更新操作,而是错误。
如果使用 NewMap("service_metrics") 已经创建了带名字的 Map,仍然建议显式理解 Publish 这一层:Map 的名字是变量自身的描述,而 Publish 决定它如何加入 expvar 的全局集合。把注册代码集中在指标包的初始化入口,后续排查会更容易。
Map 的键值关系应该怎样设计

把一个 Map 当作“带名字的指标对象”后,可以用下面的规则判断键值设计:
| 场景 | 推荐操作 | 设计重点 |
|---|---|---|
| 累计请求或命中次数 | Add | 键集合有限,值按增量增长 |
| 当前深度、当前连接数 | Set | 每次写入代表最新快照 |
| 读取单个业务维度 | Get | 先处理不存在的键 |
| 下线某个动态维度 | Delete | 只删除不再需要的键,避免误删整个 Map |
| 导出或遍历全部维度 | Do | 遍历期间仍要考虑业务更新带来的变化 |
尤其要避免把用户 ID、订单号等高基数字段当作 Map 的键。expvar 适合公开有限、可聚合的运行时统计,不适合承载明细事件流。
并发更新时要记住哪些边界
expvar 文档说明,公开变量的更新操作是原子的,因此多个请求并发调用 Add、Set 等操作时,不需要为每一次简单更新额外包一层互斥锁。但“一个业务动作包含多次更新”仍然不是自动事务。例如先增加成功数、再删除临时键的组合,不能因为单个 Map 方法是原子的,就推断整组动作具备原子性。
func recordResult(ok bool) {
// 每次只做一个原子指标更新;复合业务一致性应由调用方另行设计。
if ok {
serviceMetrics.Add("success", 1)
return
}
serviceMetrics.Add("failure", 1)
}
func removeDeprecatedKey() {
// Delete 删除一个业务键,不会影响同一 Map 中的其他键。
serviceMetrics.Delete("legacy_requests")
}
遍历 Map 时也要区分“读取全部键”和“冻结一份快照”。Do 适合把当前公开变量逐项交给回调,但如果调用方要在多个后续步骤中保持同一批数据,就应在自己的代码中复制需要的字段,不要把回调过程当作长期快照。
一套可复用的落地顺序
- 先列出有限的业务维度,并明确每个键代表累计值还是当前快照。
- 创建唯一的
expvar.Map,让所有同类指标共享一个顶层名称。 - 在包初始化阶段调用一次
expvar.Publish,避免请求路径重复注册。 - 累计类指标使用
Add,替换类指标使用Set,读取前用Get处理缺失键。 - 对动态键设置数量上限,必要时使用
Delete回收已经退出的维度。 - 由现有 expvar HTTP 入口读取公开变量,关注返回结构是否仍然符合监控方的键名约定。
这套顺序的重点不是把所有业务数据都塞进指标,而是把“可聚合、低基数、可长期观察”的运行时状态稳定地组织起来。Publish 负责对外命名,Map 负责内部维度;两层职责分开后,后续增加一个指标键通常不需要改动监控入口。
常见问题
为什么 Publish 同名会出错?
因为 expvar 全局表要求顶层名称唯一。相同名称通常意味着初始化代码重复或包边界没有划清,应保留一个注册点,而不是继续重试发布。
Map 里可以放自定义指标吗?
可以,只要自定义类型实现 expvar.Var 所需的方法。标准的 Int、Float 等类型适合常见统计;自定义类型要特别注意输出格式和并发保护。
为什么监控结果里看不到新键?
先区分是没有写入键、写入发生在错误的 Map,还是读取端缓存了旧结果。代码层面重点检查 Map 实例是否唯一、Publish 是否在初始化阶段执行,以及业务键是否与读取端约定完全一致。
速查表
| 目标 | API | 注意事项 |
|---|---|---|
| 注册一个公开指标组 | expvar.Publish(name, map) | 顶层名称必须唯一 |
| 累计一个键 | map.Add(key, delta) | 适合 Int 型累计量 |
| 替换一个键 | map.Set(key, value) | value 实现 Var |
| 读取一个键 | map.Get(key) | 处理 nil |
| 移除一个键 | map.Delete(key) | 避免高基数键长期增长 |
Go template.Funcs 怎样在 ParseFS 前注册自定义函数
- 上一篇
- Go template.Funcs 怎样在 ParseFS 前注册自定义函数
- 下一篇
- Go embed FS 打开不存在资源时怎样保留错误上下文
-
- Golang · Go问答 | 1小时前 | go · RGBA image ColorModel At
- Go image 怎样读取不同颜色模型的像素
- 291浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 怎样把 pprof 挂到独立 ServeMux
- 491浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go embed FS 打开不存在资源时怎样保留错误上下文
- 141浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go template.Funcs 怎样在 ParseFS 前注册自定义函数
- 406浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go XML Decoder Strict 关闭后适合什么兼容场景
- 412浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 怎样读取子进程资源使用量
- 373浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go os/user Current 和 Lookup 怎样选择
- 171浏览 收藏
-
- Golang · Go问答 | 1小时前 | 权限 · Go问答 · Go Unix Socket UnixConn 对端凭据 SO_PEERCRED x/sys/unix
- Go Unix socket 怎样读取对端凭据
- 302浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go Unix 数据报套接字怎样处理报文边界
- 336浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 项目怎样为 HTTP/3 客户端划分可选依赖
- 471浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go HTTP/2 异常怎样用 GODEBUG 获取线索
- 366浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go PGO 怎样合并多个代表性 profile
- 122浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 474次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 532次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 551次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 505次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 324次使用
-
- Go runtime/metrics.Float64Histogram 如何读取延迟分布:桶边界、计数快照与百分位估算
- 2026-08-30 197浏览
-
- Go 1.26 runtime/metrics 调度指标怎么选:runnable、waiting 与线程数的诊断边界
- 2026-09-04 405浏览
-
- Go runtime/metrics 怎么按指标描述选择正确的 Value 类型
- 2026-10-12 203浏览
-
- Go 问答:为什么并发读写 map 会 panic,sync.Map 和锁该怎么选
- 2026-06-12 109浏览
-
- Go 问答:defer 为什么不适合直接放在大循环里,资源该怎么释放
- 2026-06-12 418浏览
