翻譯自<A theory of modern Go> by Peter Bourgon 2017/06/09html
原文連接git
全文結論:github
全局狀態會產生巨大的反作用 ——> 須要避免包級別的變量和init函數sql
Go is easy to read數據庫
Go語言惟一最佳的屬性是基本上沒有什麼魔法代碼。除了極少數的例外外,直接閱讀Go的源碼不會產生諸如「定義」,「依賴關係」,「運行時行爲」的歧義,而這讓Go的可讀性較好,從而使得Go代碼較容易維護,這是工業化編程的最高境界。編程
Magic is badide
可是魔法代碼仍然有一些方式混入其中。不幸的是,很是廣泛的一種方式是經過使用全局狀態。包級別的全局對象能夠對外部調用者隱藏狀態和行爲。調用這些全局變量的代碼可能會產生意外的反作用,從而破壞了讀者理解和腦海中構建程序的能力。函數
函數(包括方法,在go中兩者略有不一樣)基本上是Go用來構建抽象的惟一機制。測試
思考如下函數定義:翻譯
func NewObject(n int) (*Object, error)
按照慣例來說,咱們但願形式爲NewXxx的函數是類型構造函數。而這個函數也確實是構造函數,由於咱們看到函數返回指向對象的指針和錯誤。由此咱們能夠推斷出構造函數可能構形成功也可能構造失敗,若是構造失敗,將收到error告訴咱們緣由。
該構造函數參數爲單int,咱們假定該int參數控制了函數返回對象Object的生成。咱們假定對參數int n有一些約束,若是不知足約束將致使錯誤。可是因爲該函數不接受其餘參數,所以咱們但願它除了分配內存外應該沒有其餘反作用。
僅經過閱讀函數簽名,咱們就能夠獲得這些推論,腦海中大概就有此函數了。從main函數的第一行開始重複遞歸的應用這個過程,是咱們閱讀和理解程序的方式。
假定這是NewObject函數的實現:
func NewObject(n int) (*Object, error) { row := dbconn.QueryRow("SELECT ... FROM ... WHERE ...") var id string if err := row.Scan(&id); err != nil { logger.Log("during row scan: %v", err) id = "default" } resource, err := pool.Request(n) if err != nil { return nil, err } return &Object{ id: id, res: resource, }, nil }
該函數調用了:
1.包級別的全局變量 database / sql.Conn,以對某些未指定的數據庫進行查詢;
2.包級別的全局記錄器,用於將任意格式的字符串輸出到某個位置;
3.以及包級別的某種類型的連接池對象,以請求某種類型的資源。
全部這些操做都有反作用,而這些反作用從函數簽名則徹底不可見。調用者沒有辦法預測這些事情發生,除非經過閱讀函數體並跳到全部全局變量的定義處查看。
考慮另外一種形式的簽名函數:
func NewObject(db *sql.DB, pool *resource.Pool, n int, logger log.Logger) (*Object, error)
經過將每一個全局依賴做爲參數,咱們使讀者能夠準確地知道函數的做用範圍和在函數體內可能發生的行爲。調用者確切地知道該函數須要什麼參數,並能夠提供這些參數。
若是咱們正在爲此程序設計公共API,咱們甚至能夠採起更有效的措施。
// RowQueryer models part of a database/sql.DB. type RowQueryer interface { QueryRow(string, ...interface{}) *sql.Row } // Requestor models the requesting side of a resource.Pool. type Requestor interface { Request(n int) (*resource.Value, error) } func NewObject(q RowQueryer, r Requestor, n int, logger log.Logger) (*Object, error) { // ... }
經過將每一個具體對象抽象爲接口,及僅捕獲函數中使用到的方法,咱們容許調用者本身去實現。這減小了包之間的源碼級耦合,並使咱們可以模擬測試中的具體依賴關係。若是對使用具體的包級別全局變量代碼進行測試,咱們會發現這種作法是多麼乏味且容易出錯。
若是咱們全部的構造函數和其餘函數都顯式地接受了它們的依賴關係,那麼全局變量就沒有任何用處。相反,咱們能夠在主函數中構造全部數據庫鏈接,日誌記錄,連接池,以便 未來的讀者能夠很是清楚地繪製出組件圖並使用。
並且,咱們能夠很是明確地將這些依賴關係傳遞給使用它們的組件/函數,從而不會對全局變量感到困惑。另外值得注意的是,若是沒有全局變量,那麼也就再也不須要使用init函數了,init函數的惟一目的是實例化或改變包級別的全局狀態。
Try to write go without global state
編寫幾乎沒有全局狀態的Go程序不只可能,並且很是容易。以個人經驗來看,以這種方式編程不會比使用全局變量縮小函數定義慢或乏味。
相反,當函數簽名可靠且完整地描述了函數主體的做用範圍時,咱們能夠更高效地進行代碼推理,重構和維護。 Go kit從一開始就以這種風格編寫,並所以受益。
Avoid two things
綜上所述,咱們能夠發展出現代Go理論。根據 Dave Cheney所述,提出如下準則:
固然也存在例外。