這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
寫在前面
expvar包是 Golang 官方提供的公開變數包,它可以輔助調試全域變數。支援一些常見的類型:float64、int64、Map、String。如果我們的程式要用到上面提的四種類型(其中,Map 類型要求 Key 是字串)。可以考慮使用這個包。
功能
- 它支援對變數的基本操作,修改、查詢這些;
- 整形類型,可以用來做計數器;
- 操作都是安全執行緒的。這點很不錯。相信大家都自己整過全域變數,除了變數還得整的鎖,自己寫確實挺麻煩的;
- 此外還提供了調試介面,
/debug/vars。它能夠展示所有通過這個包建立的變數;
所有的變數都是Var類型,可以自己通過實現這個介面擴充其它的類型;
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 }
Handler()方法可以得到調試介面的http.Handler,和自己的路由對接。
這些基礎的功能就不多說了,大家可以直接看官方的文檔。
調試介面
看源碼的時候發現一個非常有意思的調試介面,/debug/vars會把所有註冊的變數列印到介面裡面。這個介面很有情懷。
func init() { http.HandleFunc("/debug/vars", expvarHandler) Publish("cmdline", Func(cmdline)) Publish("memstats", Func(memstats))}
源碼
var ( mutex sync.RWMutex vars = make(map[string]Var) varKeys []string // sorted)
varKeys是全域變數所有的變數名,而且是有序的;
vars根據變數名儲存了對應的資料。當然mutex就是這個 Map 的鎖;
這三個變數組合起來其實是一個有序安全執行緒雜湊表的實現。
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 } type Int struct { i int64 } func (v *Int) Value() int64 { return atomic.LoadInt64(&v.i) } func (v *Int) String() string { return strconv.FormatInt(atomic.LoadInt64(&v.i), 10) } func (v *Int) Add(delta int64) { atomic.AddInt64(&v.i, delta) } func (v *Int) Set(value int64) { atomic.StoreInt64(&v.i, value) }
- 這個包裡面的所有類型都實現了這個介面;
以 Int 類型舉例。實現非常的簡單,注意Add和Set方法是安全執行緒的。別的類型實現也一樣
func Publish(name string, v Var) { mutex.Lock() defer mutex.Unlock() if _, existing := vars[name]; existing { log.Panicln("Reuse of exported var name:", name) } vars[name] = v varKeys = append(varKeys, name) sort.Strings(varKeys) } func NewInt(name string) *Int { v := new(Int) Publish(name, v) return v }
將變數註冊到一開始介紹的vars和varKeys裡面;
- 註冊時候也是安全執行緒的,所有的變數名在註冊的最後排了個序;
建立對象的時候會自動註冊。
func Do(f func(KeyValue)) { mutex.RLock() defer mutex.RUnlock() for _, k := range varKeys { f(KeyValue{k, vars[k]}) } } 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") } func Handler() http.Handler { return http.HandlerFunc(expvarHandler) }
Do方法,利用一個閉包,按照varKeys的順序遍曆所有全域變數;
expvarHandler方法是http.Handler類型,將所有變數通過介面輸出,裡面通過Do方法,把所有變數遍曆了一遍。挺巧妙;
- 通過
http.HandleFunc方法把expvarHandler這個外部不可訪問的方法對外,這個方法用於對接自己的路由;
輸出資料的類型,fmt.Fprintf(w, "%q: %s", kv.Key, kv.Value),可以發現,值輸出的字串,所以輸出的內容是String()的結果。這裡有一個技巧,雖然調用的字串的方法,但是由於輸出格式%s外面並沒有引號,所有對於 JSON 來說,輸出的內容是物件類型。相當於在 JSON 編碼的時候做了一次類型轉換。
type Func func() interface{} func (f Func) Value() interface{} { return f() } func (f Func) String() string { v, _ := json.Marshal(f()) return string(v) } func cmdline() interface{} { return os.Args }
這是一個非常有意思的寫法,它可以把任何類型轉換成Var類型;
Func定義的是函數,它的類型是func() interface{}
Func(cmdline),使用的地方需要看清楚,參數是cmdline而不是cmdline(),所以這個寫法是類型轉換。轉換完之後cmdline方法就有了String()方法,在String()方法裡又調用了f(),通過 JSON 編碼輸出。這個小技巧在前面提到的http.HandleFunc裡面也有用到,Golang 的程式員對這個是真愛,咱們編碼的時候也要多用用啊。
不足
感覺這個包還是針對簡單變數,比如整形、字串這種比較好用。
- 前面已經說了,Map 類型只支援 Key 是字串的變數。其它類型還得自己擴充,擴充的話鎖的問題還是得自己搞。而且 JSON 編碼低版本不支援 Key 是整形類型的編碼,也是個問題;
Var介面太簡單,只有一個String()方法,基本上只能輸出變數所有內容,別的東西都沒辦法控制,如果你的變數有10000個索引值對,那麼這個介面基本上屬於不能用。多說一句,這是 Golang 設計的常見問題,比如日誌包,輸出的類型是io.Writer,而這個介面只支援一個方法Write([]byte),想擴充日誌包的功能很難,這也失去了抽象出來一個介面的意義。
- 路由裡面還預設追加了啟動參數和
MemStats記憶體相關參數。我個人覺得後面這個不應該加,調用runtime.ReadMemStats(stats)會引起 Stop The World,總感覺不值當。
總結
看到就寫了,並沒有什麼沉澱,寫得挺亂的。這個包很簡單,但是裡面還是有些可以借鑒的編碼和設計。新版本的 Golang 已經能解析整形為 Key 的雜湊表了,這個包啥時候能跟上支援一下?