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实现的时候,它会自动带上 cmdline
和 memstats
这两节:
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的作用和实现,用起来吧!
更多文章
本站热门
- socks5 协议详解
- zerotier简明教程
- 搞定面试中的系统设计题
- frp 源码阅读与分析(一):流程和概念
- 用peewee代替SQLAlchemy
- Golang(Go语言)中实现典型的fork调用
- DNSCrypt简明教程
- 一个Gunicorn worker数量引发的血案
- Golang validator使用教程
- Docker组件介绍(一):runc和containerd
- Docker组件介绍(二):shim, docker-init和docker-proxy
- 使用Go语言实现一个异步任务框架
- 协程(coroutine)简介 - 什么是协程?
- SQLAlchemy简明教程
- Go Module 简明教程