Go語言編程模式
在2016年倫敦舉辦的QCon大會上,Peter Bourgon做了《六年Go語言設計經驗》的報告,重點探討了在使用Go進行開發時的編程模式和反模式。在這里,我們將他給Go開發者的建議進行了簡單的總結。
GOPATH:將 GOPATH/bin 添加到“PATH”這個環境變量中,以便Go應用可以訪問所需要的二進制文件。在絕大多數場景下,Bourgon建議使用全局唯一的 GOPATH 。有些開發者希望嚴格區分自己的代碼和外部依賴代碼,這些人更傾向于創建兩個 GOPATH 條目。開發者也可以選擇不設置環境變量,并針對每個工程都使用 gb 構建。
代碼倉庫的結構:代碼倉庫的結構依賴于項目結構。如果是私人項目,開發者可以選擇自己喜歡的任何結構。如果是開源項目,開發者最好遵循Remote Packages的建議,以便 go get 命令引用該項目的包。Bourgon建議創建一個基礎目錄,其中要包含程序的主要構件,以及放置幫助包的子目錄,具體如下圖所示:
代碼格式化:Bourgon強調開發者需要重視Go的權威的代碼格式化風格,一旦開發者習慣這種風格,他的代碼的可讀性將大大提高。按照Bougron的觀點,Go開發者社區會認為非格式化的代碼出自計算機新手。每次保存之前,可以使用 gofmt 工具格式化代碼。他認為Go代碼審核指南為開發者和代碼審核者提供了一套通用的實踐規則。他還支持Andrew Gerrand關于Go開發的建議,包括如何為變量、函數和exports等元素命名,如果你能夠遵循這些建議,閱讀你代碼的人將會非常感激你。
配置:Bourgon建議配置管理應該有“清晰的定義和良好的文檔支持。”他仍舊在使用來自標準庫的 flag 包,不過也希望這個包能夠更簡單易懂。他強調了明確定義配置項的重要性。通過環境變量傳遞配置項并沒有為應用的使用者提供足夠的信息去理解應用的參數使用,他建議在 help 中提供必要的配置信息。
包名:應該根據某個模塊提供的服務而不是它的內容來定義包名。如果一個包含有 HelloWorld 消息,那么它不應該被稱為 common 或 consts ,而是 greetings 。包名應該表明它所做什么,而不是它有什么。
點導入:Bourgon建議不要使用“點導入”,這個特性通過設置點號來代替包名,使得開發者不需要明確的包名就可以訪問相應包中的變量。這個特性降低了項目的可讀性,尤其對于新手,新來的開發人員容易弄錯哪個變量屬于哪個包。Go——顯式聲明優于隱式聲明。
Flags:Bourgon并不認為在 init() 方法而不在 main() 方法中初始化flags是一個好主意,因為這使得這些flags無法在全局領域使用,而某些測試用例要用到這些flags。
構造函數:在談到構造函數時,他建議將初始化的 struct 以內聯方式直接作為參數傳入,從而避免傳入無效或者未完成的狀態,例如:
foo := newFoo(*fooKey, fooConfig{
Bar: bar,
Baz: baz,
Period: 100 * time.Millisecond,
})
有意義的默認值:不要使用 nil 初始化某個變量,這使得每次在使用該變量的時候都需要進行空值檢查,最好使用一個無操作值(no-operation value)進行變量初始化。例如,使用 ioutil.Discard 初始化一個 output 變量。
模塊的交叉引用:有些情況下會出現兩個互相引用的模塊。在構建其中的一個時,同時需要構建另一個模塊,在構建后一個時又需要第一個先構建,下列兩個 structs 的定義就屬于這種情況:
type bar struct {
baz *baz
}
type baz struct {
bar *bar
}
Bourgon提供了三種方法處理這種情況:
- 整合:兩個關系如此密切的對象應該整合成一個,在這種情況下應該整合成一個 barbaz 結構體。
- 分割:如果這兩個模塊必須保持分割,那么可以應用下列代碼中采取的策略:
type bar struct {
a *atom
monad
}
type baz struct {
atom
m *monad
}
a := &atom{...}
m := newMonad(...)
bar := newBar(a, m, ...)
baz := newBaz(a, m, ...)
- 通信:當上述兩種方法都不適用時,可以考慮在兩個模塊之間發送消息。
type bar struct {
toBaz chan<- event
}
type baz struct {
fromBar <-chan event
}
c := make(chan event)
bar := newBar(c, ...)
baz := newBaz(c, ...)
依賴:Bourgon還提出了”明確依賴關系“的建議,例如:
func (f *foo) process() {
log.Printf("bar: %v", result) // ...
}
應該寫成下面這樣:
func (f *foo) process() {
f.Logger.Printf("bar: %v", result) // ...
}
log.Printf 實際上調用了 Logger 模塊,這么寫的話就隱去了這層依賴關系。為了明確這層依賴關系,開發者應該在構造過程中創建一個 Logger 對象,并使用 ioutil.Discard 代替空值 nil 。
通道(Channel):Bourgon建議,當多個協程(goroutine)之間共享內存時應使用mutex,并通過通道對協程進行協調。
日志打印:日志記錄的代價很高,有可能成為應用的性能瓶頸。因此,建議只在絕對必要的地方記錄日志,包括給開發者閱讀或者供機器調用的信息。僅僅需要記錄 info 和 debug 級別的日志。
監控工具:Bourgon認為Go應用的監控代價很小,推薦開發者使用Prometheus監控自己應用使用的各種資源。
全局狀態:消除隱式的全局依賴和全局狀態。
測試:執行包級別的測試。為了測試而設計:使用函數式編程風格——使用參數表明依賴關系、使用接口以及避免依賴全局狀態。
依賴管理:將所有依賴項都拷貝到項目的倉庫中用于構建二進制代碼。Bourgon建議開發者根據自己的需要從 gvt 、 vendetta 、 glide 或 gb 這幾個工具中選擇。
構建:不要使用 go build ,要使用 go install ,因為后者可以緩存依賴關系,并把這些依賴關系放在 GOPATH/bin 下以便于調用。
這些建議已經被應用于開發 Go Kit ,一款用于構建微服務的分布式編程工具。
2009年以來,Bourgon在SoundCloud和Weaveworks兩家公司都使用Go語言開發,開發了幾款產品,包括:Roshi——一款基于時間序列的事件數據庫,以及Go Kit。
2016年QCon大會上的《六年Go語言設計經驗》視頻將會在今年晚些時候對外公開。