惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

宝玉的分享
宝玉的分享
J
Java Code Geeks
S
SegmentFault 最新的问题
L
LangChain Blog
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
量子位
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
雷峰网
雷峰网
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
MongoDB | Blog
MongoDB | Blog
Microsoft Security Blog
Microsoft Security Blog
腾讯CDC
H
Help Net Security
阮一峰的网络日志
阮一峰的网络日志
Jina AI
Jina AI
N
Netflix TechBlog - Medium
A
About on SuperTechFans
博客园 - 叶小钗
美团技术团队
人人都是产品经理
人人都是产品经理
D
DataBreaches.Net

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例)
Golang expvar库源码阅读
Jiajun Huang · 2020-04-12 · via Jiajun的技术笔记

最近看到同事用一个库,叫做 expvar,这个库的作用官网描述如下:

Package expvar provides a standardized interface to public variables, such as operation counters in servers.
It exposes these variables via HTTP at /debug/vars in JSON format. 

意思是这个库用来暴露一些公共的变量,他会自动注册到 /debug/vars 这个路径下。我们来看看基本用法:

package main

import (
        "expvar"
        "net/http"
)

func main() {
        http.ListenAndServe(":8080", expvar.Handler())
}

// 或者如下
package main

import (
        _ "expvar"
        "net/http"
)

func main() {
        http.ListenAndServe(":8080", nil)
}

请求一下就可以看到输出:

$ http :8080  # 如果是第二种代码,这里的命令应该改成 `http :8080/debug/vars`
HTTP/1.1 200 OK                               
Content-Type: application/json; charset=utf-8
Date: Sun, 12 Apr 2020 01:22:14 GMT
Transfer-Encoding: chunked   
                              
{                            
    "cmdline": [              
        "/tmp/go-build998114494/b001/exe/main"
    ],                       
    "memstats": {                
        "Alloc": 246696,     
        "BuckHashSys": 3565, 
        "BySize": [           
            {                 
                "Frees": 0,   
                "Mallocs": 0, 
                "Size": 0    
            },                
            {                  
                "Frees": 0,   
                "Mallocs": 15,
                "Size": 8    
            },              
...

这是因为expvar实现的时候,它会自动带上 cmdlinememstats 这两节:

func cmdline() interface{} {
        return os.Args
}

func memstats() interface{} {
        stats := new(runtime.MemStats)
        runtime.ReadMemStats(stats)
        return *stats
}

func init() {
        http.HandleFunc("/debug/vars", expvarHandler)
        Publish("cmdline", Func(cmdline))
        Publish("memstats", Func(memstats))
}

我们可以加上自己想要展示的公共变量:

package main

import (
        "expvar"
        "net/http"
        "time"
)

func main() {
        lastAccess := expvar.NewString("lastAccess")
        http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
                w.Write([]byte("hello"))
                lastAccess.Set(time.Now().String())
        })

        http.ListenAndServe(":8080", nil)
}

测试效果:

$ http :8080/debug/vars | grep lastAccess
"lastAccess": "2020-04-12 09:52:14.127334409 +0800 CST m=+15.783055827",
$ http :8080/
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain; charset=utf-8
Date: Sun, 12 Apr 2020 01:52:27 GMT

hello

$ http :8080/debug/vars | grep lastAccess
"lastAccess": "2020-04-12 09:52:27.533390067 +0800 CST m=+29.189111480",

可以看到,每次我们都能看到最近访问时间。

源码阅读

我们要知其然知其所以然,来看看expvar是怎么实现的吧!为了方便,我把阅读思路也写在了注释里面。

// 从 NewString 入手
lastAccess := expvar.NewString("lastAccess")

func NewString(name string) *String {
	v := new(String)
	Publish(name, v)
	return v
}

// 看看String的定义
type String struct {
	s atomic.Value // string
}

// 看看Publish的定义
// Publish declares a named exported variable. This should be called from a
// package's init function when it creates its Vars. If the name is already
// registered then this will log.Panic.
func Publish(name string, v Var) {
	if _, dup := vars.LoadOrStore(name, v); dup {
		log.Panicln("Reuse of exported var name:", name)
	}
	varKeysMu.Lock()
	defer varKeysMu.Unlock()
	varKeys = append(varKeys, name)
	sort.Strings(varKeys)
}

// 可以看到 Var 是一个接口
// Var is an abstract type for all exported variables.
type Var interface {
	// String returns a valid JSON value for the variable.
	// Types with String methods that do not return valid JSON
	// (such as time.Time) must not be used as a Var.
	String() string
}

// vars 和 varKeys 的定义是这样的
// All published variables.
var (
	vars      sync.Map // map[string]Var
	varKeysMu sync.RWMutex
	varKeys   []string // sorted
)

// 所以可以看到,逻辑就是每次把数值存储到 `vars`,`vars` 是一个map[string]Var类型的map。因为map迭代时是无序的,
// 所以有 `varKeys` 用来排序,这样输出的时候,就可以每次都有序输出

// 来看看如何暴露这些变量
func expvarHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	fmt.Fprintf(w, "{\n")
	first := true
	Do(func(kv KeyValue) {
		if !first {
			fmt.Fprintf(w, ",\n")
		}
		first = false
		fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value)
	})
	fmt.Fprintf(w, "\n}\n")
}

// 可以看到,很粗暴,先输出 `{\n`,然后输出key value的字符串,最后输出 `\n}\n`,相当于代码拼接JSON
// Do这个函数用来迭代
// Do calls f for each exported variable.
// The global variable map is locked during the iteration,
// but existing entries may be concurrently updated.
func Do(f func(KeyValue)) {
	varKeysMu.RLock()
	defer varKeysMu.RUnlock()
	for _, k := range varKeys {
		val, _ := vars.Load(k)
		f(KeyValue{k, val.(Var)})
	}
}

// 就如我们前面所说,按 varKeys 的顺序来迭代,然后依次执行传入的函数

至此我们了解了expvar的作用和实现,用起来吧!