当前位置:首页 > 文章列表 > Golang > Go教程 > Go expvar.Publish 怎么暴露自定义运行指标

Go expvar.Publish 怎么暴露自定义运行指标

来源:17golang原创 2026-10-04 16:24:57 0浏览 收藏

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 等修改操作是原子的,适合直接放在并发请求路径里。

业务处理器、Int 计数器、Map 分类计数、Func 快照、全局注册表和 debug vars 路由的静态模块关系
图1:业务更新对象、expvar 全局注册表与 JSON 输出端点的静态结构图,不是运行截图。

先选变量类型,避免把所有状态塞进一个 Func

类型适合内容更新方式注意点
expvar.Int请求数、进行中请求数Add、SetNewInt 创建时已经注册
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}'
expvar Publish、Func 回调、有效 JSON、唯一名称、内部路由和采集器之间的静态依赖关系
图2:Publish 注册契约、读取时计算与访问边界的静态依赖图,不代表某次真实请求结果。

回归检查:迁移后重点看六个风险

  • 名称唯一:同一个进程里的所有发布变量共用注册表,重复名称会直接 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 才真正减少维护成本,而不是把一个自制接口换成另一个不可控入口。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go String 方法里调用 fmt 为什么可能无限递归Go String 方法里调用 fmt 为什么可能无限递归
上一篇
Go String 方法里调用 fmt 为什么可能无限递归
Git worktree 怎么清理失效工作区记录
下一篇
Git worktree 怎么清理失效工作区记录
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    326次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    385次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    376次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    343次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    170次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码