10

Golang expvar库源码阅读

 4 years ago
source link: https://jiajunhuang.com/articles/2020_04_12-expvar.md.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK