指南
https://google.github.io/styleguide/go/guide
注意: 本文件屬於 Google 內部 Go 風格 系列文件之一。本份文件具有 規範性 與 典範性。詳細說明請見總覽。
風格原則
下列幾項概括性原則,描述了我們認為「易讀的 Go 程式碼」應如何思考與撰寫。下列為易讀程式碼的特徵,依重要性排序:
- 清晰性 (Clarity):程式碼的目的與背後理由,對讀者來說一目了然。
- 簡潔性 (Simplicity):程式碼以最簡單的方式達成目標。
- 精煉性 (Concision):程式碼具有高訊號雜訊比。
- 可維護性 (Maintainability):程式碼撰寫方式有利於後續維護。
- 一致性 (Consistency):程式碼與更廣泛的 Google 程式碼庫保持一致。
清晰性
可讀性的核心目標,是寫出對讀者來說清楚易懂的程式碼。
清晰性主要透過有效的命名、有幫助的註解,以及高效的程式碼組織來達成。
清晰性必須以「讀者」而非「程式碼撰寫者」的角度來看待。程式碼易讀比易寫更重要。程式碼的清晰性可分為兩個面向:
這段程式碼實際上在做什麼?
Go 的設計使得讀者通常能相對直觀地看出程式碼在做什麼。在語意不夠明確、或讀者需要先備知識才看得懂的情境下,值得花時間讓程式碼的目的對未來的讀者更清楚。例如可以:
- 採用更具描述性的變數名稱
- 增加額外的註解
- 用空白與註解來分段
- 將程式碼重構為獨立的函式或方法,使其更模組化
這裡沒有放諸四海皆準的方法,但在開發 Go 程式碼時,把清晰性列為優先考量是非常重要的。
這段程式碼為什麼這樣寫?
程式碼背後的理由,通常透過變數、函式、方法或套件的命名就能充分表達。當命名不足以表達時,加上註解就很重要。當程式碼包含讀者可能不熟悉的細微之處時,「為什麼?」尤其重要,例如:
- 語言層級的細微之處,例如 closure 會擷取迴圈變數,而 closure 出現的位置距離變數定義很遠
- 商業邏輯層級的細微之處,例如某個存取控制檢查需要區分「真正的使用者」與「冒用使用者身分的對象」
某些 API 在使用上需要小心。例如為了效能,程式碼可能寫得錯綜複雜難以追蹤;或一連串複雜的數學運算以非預期的方式使用型別轉換。在這類情境 (以及其他諸多情況) 中,搭配的註解與文件必須說明這些面向,讓未來的維護者不會誤用,讀者也不需要靠逆向工程才能理解。
同時也要留意,某些試圖增加清晰性的做法 (例如多寫註解) 反而可能讓程式碼意圖更模糊:增加雜訊、把程式碼已經說過的話重述一遍、與程式碼互相矛盾,或是製造保持註解最新的維護負擔。請讓程式碼自己說話 (例如,讓符號名稱本身就具有自我描述能力),而不是寫多餘的註解。註解通常更適合解釋「為什麼這樣做」,而不是「程式碼在做什麼」。
Google 的程式碼庫整體上相當一致。當某段程式碼明顯與眾不同 (例如使用了不熟悉的模式),通常都是有充分理由的,常見原因是效能。維持這項特性,有助於讀者在閱讀新程式碼時清楚知道應該把注意力放在哪裡。
標準函式庫中包含許多體現此原則的範例,其中包括:
package sort內給維護者看的註解。- 同一套件中良好的可執行範例,它同時嘉惠使用者 (這些範例會顯示在 godoc 中) 與維護者 (這些範例會作為測試的一部分執行)。
strings.Cut雖然只有四行程式碼,卻能改善呼叫端的清晰性與正確性。
簡潔性
你的 Go 程式碼,對於使用、閱讀與維護它的人來說都應該是簡潔的。
Go 程式碼應以能達成目標的最簡單方式撰寫,行為與效能皆然。在 Google 的 Go 程式碼庫中,簡潔的程式碼具有下列特徵:
- 由上而下閱讀都容易理解
- 不假設讀者已經知道它在做什麼
- 不假設讀者能記住前面所有的程式碼
- 不會有多餘的抽象層
- 命名不會讓人對平凡的事物投入過多注意
- 讓資料流與決策的傳遞對讀者保持清楚
- 註解說明的是「為什麼」而不是「做什麼」,以避免日後偏離
- 文件可獨立成立、不需要外部說明
- 具有有用的錯誤訊息與有用的測試失敗訊息
- 通常與「炫技」的寫法是互斥的
「程式碼本身的簡潔」與「API 使用上的簡潔」之間可能會有取捨。例如,有時候讓程式碼複雜一些是值得的,因為這樣 API 的使用者更容易正確地呼叫它;反過來,有時候則值得把一點額外工作留給 API 使用者,以保持程式碼本身的簡單與易懂。
當程式碼確實需要複雜性時,複雜性應該是經過刻意取捨後加入的。通常會在需要額外效能、或某個函式庫/服務有多個截然不同使用者的情境出現。複雜性可能是合理的,但加入時應同時附上文件,讓使用者與未來的維護者能理解並順利探索這些複雜性。最好再補上測試與範例,展示其正確用法,尤其是當程式碼同時存在「簡單寫法」與「複雜寫法」時。
這個原則並不是說 Go 不能或不應該寫複雜程式碼,也不是說 Go 程式碼不允許複雜。我們追求的是一個「不必要的複雜性盡量避免」的程式碼庫,如此一來,當複雜性確實出現時,就代表這段程式碼需要特別小心理解與維護。理想上,旁邊應該有註解說明背後的理由,並指出需要留意的地方。這常常出現在針對效能進行最佳化的程式碼中:這通常需要更複雜的做法,例如預先配置一個緩衝區並在 goroutine 整個生命週期內重複使用。當維護者看到這種寫法時,就應該意識到這段程式碼對效能很敏感,在進行未來變更時就會更加謹慎。反之,如果不必要地引入這種複雜性,反而會成為未來閱讀或修改程式碼者的負擔。
如果某段程式碼最終變得非常複雜,但它的目的本應簡單,這往往是一個訊號:應該回頭重新檢視實作,看看是否有更簡單的方式可以達成相同目標。
最小機制原則
當有多種方式可以表達同一概念時,優先選擇使用最標準工具的那一種。複雜的機制雖然存在,但不應在沒有理由的情況下使用。在程式碼中加入複雜度很容易,但要在事後發現某段複雜度其實多餘並將其移除,卻困難得多。
- 若核心語言結構 (例如 channel、slice、map、迴圈、struct) 已能滿足需求,優先使用它。
- 若沒有,則找標準函式庫中的工具 (例如 HTTP client 或樣板引擎)。
- 最後,在引入新相依套件或自行打造之前,考慮 Google 程式碼庫中是否已有合適的核心函式庫。
舉例來說,若有一段正式環境的程式碼用 flag 綁定了一個帶預設值的變數,而測試時必須覆寫這個值。除非你打算測試的是程式本身的命令列介面 (例如透過 os/exec),否則直接覆寫該變數會比呼叫 flag.Set 更簡單,也因此更可取。
同樣地,若一段程式碼需要做集合成員檢查,通常一個布林值的 map (例如 map[string]bool) 就夠用了。只有在需要比 map 更複雜、或無法用 map 簡單完成的操作時,才需要使用提供集合型別與功能的函式庫。
精煉性
精煉的 Go 程式碼具有高訊號雜訊比。讀者可以輕易辨識出重要細節,而命名與結構則引導讀者一步步看完這些細節。
許多事情會妨礙重要細節在當下被凸顯出來:
- 重複的程式碼
- 多餘的語法
- 語意不明的命名
- 不必要的抽象
- 空白
重複的程式碼尤其容易把「幾乎相同的多段程式碼」中真正不同的細節給掩蓋掉,迫使讀者必須以肉眼比對相似的多行程式碼才能找出差異。表格驅動測試 (Table-driven testing) 就是一個例子,它能精煉地把共通程式碼從每一輪測試的關鍵差異中抽離出來;但要把哪些東西放進表格中,將直接影響整張表格的可理解程度。
當在多種程式碼結構之間做選擇時,值得考慮哪一種寫法能讓重要細節最為明顯。
理解並使用常見的程式碼結構與慣用寫法,對於維持高訊號雜訊比同樣重要。例如,下列這段程式碼在錯誤處理中非常常見,讀者可以很快理解它的目的:
// Good:
if err := doSomething(); err != nil {
// ...
}如果某段程式碼看起來與此非常相似但其實有微妙差異,讀者可能會忽略這個變化。在這種情況下,值得刻意「boost」錯誤檢查的訊號強度,例如加上註解提醒讀者注意:
// Good:
if err := doSomething(); err == nil { // if NO error
// ...
}可維護性
程式碼被修改的次數,遠多於它被撰寫的次數。易讀的程式碼不僅對嘗試理解其運作方式的讀者要有意義,對需要動手修改它的工程師也要有意義。清晰性是關鍵。
可維護的程式碼具有下列特徵:
- 未來的工程師能夠正確且輕易地修改它
- 它的 API 結構能夠隨需求平順地擴充
- 對所做的假設保持清楚,並選擇與「問題結構」對應的抽象,而不是與「程式碼結構」對應的抽象
- 避免不必要的耦合,並且不包含尚未被使用到的功能
- 具有完整的測試套件,能確保所承諾的行為與重要邏輯仍然正確;在失敗時能提供清楚、可付諸行動的診斷訊息
當使用 interface 與型別這類「會從使用情境抽離資訊」的抽象時,應確保它們帶來足夠的好處。當程式碼直接使用具體型別時,編輯器與 IDE 可以直接連到方法定義並顯示對應文件;但若是使用 interface,則只能跳到 interface 定義。Interface 是一個強大的工具,但也有代價:維護者可能必須了解底層實作的細節才能正確使用 interface,這就必須在 interface 的文件或呼叫端加以說明。
可維護的程式碼也避免把重要細節藏在容易被忽略的地方。例如下列每一行程式碼,某個關鍵字元的有無,都會徹底影響該行的意義:
// Bad:
// The use of = instead of := can change this line completely.
if user, err = db.UserByID(userID); err != nil {
// ...
}// Bad:
// The ! in the middle of this line is very easy to miss.
leap := (year%4 == 0) && (!(year%100 == 0) || (year%400 == 0))這兩段程式碼本身並沒有錯,但都可以寫得更明確,或加上註解凸顯關鍵行為:
// Good:
u, err := db.UserByID(userID)
if err != nil {
return fmt.Errorf("invalid origin user: %s", err)
}
user = u// Good:
// Gregorian leap years aren't just year%4 == 0.
// See https://en.wikipedia.org/wiki/Leap_year#Algorithm.
var (
leap4 = year%4 == 0
leap100 = year%100 == 0
leap400 = year%400 == 0
)
leap := leap4 && (!leap100 || leap400)同樣道理,如果一個 helper 函式把關鍵邏輯或重要邊界情況藏起來,未來修改的人就很容易漏掉這些情況。
可預測的命名,是可維護程式碼的另一項特徵。套件的使用者或程式碼的維護者,應該能在特定情境下預測某個變數、方法或函式的名字會是什麼。對於相同的概念,函式參數與接收者的名字一般應使用相同的名稱,這樣文件才容易理解,也方便日後以較低成本進行重構。
可維護的程式碼會儘量降低相依 (顯性與隱性皆然)。相依的套件越少,就代表越少行可能影響行為的程式碼。避免相依於內部或未文件化的行為,可以讓程式碼較不會在這些行為日後變動時產生維護負擔。
當思考程式碼的結構與寫法時,值得花時間想想這段程式碼可能會如何隨時間演進。如果某種寫法能讓未來的修改更容易、更安全,通常即使設計上稍微複雜一點,也是一個好的取捨。
一致性
一致的程式碼,意指它在外觀、感受與行為上都與更廣泛的程式碼庫中其他類似程式碼相符,包括團隊或套件的範圍內,甚至是同一份檔案內。
一致性的考量不會凌駕於以上所有原則之上,但若必須在原則持平時做出抉擇,通常傾向一致性的選擇較為有利。
套件內部的一致性,通常是最直接重要的一層一致性。如果同一個問題在套件中被以多種方式處理,或同一個概念在同一份檔案中出現多種名字,讀起來會相當突兀。然而,即便如此,套件內一致性仍不應凌駕於已成文的風格原則或全域一致性之上。
核心準則
下列準則匯集了 Go 風格中最重要的部分,所有 Go 程式碼都應遵循。我們期望工程師在取得「可讀性 (readability)」資格之前就已熟悉並遵循這些原則。這些準則不會頻繁變動,新增的準則也必須通過較高的審查門檻。
下列準則延伸自 Effective Go 中的建議,後者為整個社群的 Go 程式碼提供了共同的基準。
程式碼格式
所有 Go 原始碼都必須符合 gofmt 工具輸出的格式。Google 程式碼庫透過提交前檢查 (presubmit) 強制執行此格式。產生的程式碼 (Generated code) 通常也應該套用格式化 (例如使用 format.Source),因為這些程式碼同樣會在 Code Search 中被瀏覽。
MixedCaps
撰寫多字組成的名稱時,Go 原始碼採用 MixedCaps 或 mixedCaps (即駝峰式命名),而非底線 (snake case)。
即便這違反其他語言的慣例,在 Go 中仍然如此。例如一個常數,若為公開 (exported) 則名為 MaxLength (而非 MAX_LENGTH);若為非公開 (unexported) 則名為 maxLength (而非 max_length)。
在決定首字母大小寫時,區域變數 (local variable) 視為非公開。
行長度
Go 原始碼沒有固定的行長度。如果一行讀起來太長,優先重構而不是拆行。如果它已經是實務上能寫到的最短長度,就讓它保持長行。
不要因為下列原因拆行:
- 為了避免縮排變化造成的混淆 (例如函式宣告、條件判斷)
- 為了把一段長字串 (例如 URL) 塞進多行較短的內容中
命名
命名一半是科學、一半是藝術。在 Go 中,名字往往比其他語言略短,但同樣的整體準則仍適用。命名應該:
- 使用時不應感到重複
- 將上下文納入考量
- 不重複已經明確的概念
更具體的命名指引請參考決策。
區域一致性
當風格指南對某個議題沒有特別表態時,撰寫者可以自由選擇偏好的風格;除非鄰近的程式碼 (通常為同一份檔案或套件中,有時為同一個團隊或專案目錄) 對該議題已採取一致的立場。
有效的區域風格考量範例:
- 在錯誤格式化輸出時使用
%s或%v - 以帶緩衝的 channel 取代 mutex 的使用方式
無效的區域風格考量範例:
- 程式碼的行長度限制
- 採用基於 assertion 的測試函式庫
如果區域風格與風格指南牴觸,但其對可讀性的影響只限於一份檔案內,通常會在程式碼審查中被指出,但全面修正會超出該變更 (CL) 的範圍。此時可以開立一個 bug 來追蹤後續修正。
如果某項變更會讓既有的風格偏離更嚴重、讓偏離出現在更多 API 表面、讓偏離擴散到更多檔案,或實際引入 bug,那麼區域一致性就不再是違反風格指南的有效理由。在這種情況下,作者應在同一個 CL 中順手清理既有程式碼、在當前 CL 之前先做一次重構,或選擇至少不會讓區域問題惡化的替代方案。