Golang expvar库源码阅读

最近看到同事用一个库,叫做 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的作用和实现,用起来吧!


更多文章
  • Golang Map 源码阅读与分析
  • MySQL 零碎知识 - MySQL必知必会
  • Golang slice 源码阅读与分析
  • 经典好书推荐(2017)
  • Golang log库 源码阅读与分析
  • 毕业后一年
  • ansible 简明教程
  • 自己写个搜索引擎
  • HTTP 路由的两种常见设计形式
  • Golang的short variable declaration
  • Greenlet和Stackless Python
  • 写一个简单的ORM
  • 从源码看Python的descriptor
  • Python字符串格式化
  • Gunicorn 简明教程