跳至內容
指南

指南

https://google.github.io/styleguide/go/guide

總覽 | 指南 | 決策 | 最佳實踐

注意: 本文件屬於 Google 內部 Go 風格 系列文件之一。本份文件具有 規範性典範性。詳細說明請見總覽

風格原則

下列幾項概括性原則,描述了我們認為「易讀的 Go 程式碼」應如何思考與撰寫。下列為易讀程式碼的特徵,依重要性排序:

  1. 清晰性 (Clarity):程式碼的目的與背後理由,對讀者來說一目了然。
  2. 簡潔性 (Simplicity):程式碼以最簡單的方式達成目標。
  3. 精煉性 (Concision):程式碼具有高訊號雜訊比。
  4. 可維護性 (Maintainability):程式碼撰寫方式有利於後續維護。
  5. 一致性 (Consistency):程式碼與更廣泛的 Google 程式碼庫保持一致。

清晰性

可讀性的核心目標,是寫出對讀者來說清楚易懂的程式碼。

清晰性主要透過有效的命名、有幫助的註解,以及高效的程式碼組織來達成。

清晰性必須以「讀者」而非「程式碼撰寫者」的角度來看待。程式碼易讀比易寫更重要。程式碼的清晰性可分為兩個面向:

這段程式碼實際上在做什麼?

Go 的設計使得讀者通常能相對直觀地看出程式碼在做什麼。在語意不夠明確、或讀者需要先備知識才看得懂的情境下,值得花時間讓程式碼的目的對未來的讀者更清楚。例如可以:

  • 採用更具描述性的變數名稱
  • 增加額外的註解
  • 用空白與註解來分段
  • 將程式碼重構為獨立的函式或方法,使其更模組化

這裡沒有放諸四海皆準的方法,但在開發 Go 程式碼時,把清晰性列為優先考量是非常重要的。

這段程式碼為什麼這樣寫?

程式碼背後的理由,通常透過變數、函式、方法或套件的命名就能充分表達。當命名不足以表達時,加上註解就很重要。當程式碼包含讀者可能不熟悉的細微之處時,「為什麼?」尤其重要,例如:

  • 語言層級的細微之處,例如 closure 會擷取迴圈變數,而 closure 出現的位置距離變數定義很遠
  • 商業邏輯層級的細微之處,例如某個存取控制檢查需要區分「真正的使用者」與「冒用使用者身分的對象」

某些 API 在使用上需要小心。例如為了效能,程式碼可能寫得錯綜複雜難以追蹤;或一連串複雜的數學運算以非預期的方式使用型別轉換。在這類情境 (以及其他諸多情況) 中,搭配的註解與文件必須說明這些面向,讓未來的維護者不會誤用,讀者也不需要靠逆向工程才能理解。

同時也要留意,某些試圖增加清晰性的做法 (例如多寫註解) 反而可能讓程式碼意圖更模糊:增加雜訊、把程式碼已經說過的話重述一遍、與程式碼互相矛盾,或是製造保持註解最新的維護負擔。請讓程式碼自己說話 (例如,讓符號名稱本身就具有自我描述能力),而不是寫多餘的註解。註解通常更適合解釋「為什麼這樣做」,而不是「程式碼在做什麼」。

Google 的程式碼庫整體上相當一致。當某段程式碼明顯與眾不同 (例如使用了不熟悉的模式),通常都是有充分理由的,常見原因是效能。維持這項特性,有助於讀者在閱讀新程式碼時清楚知道應該把注意力放在哪裡。

標準函式庫中包含許多體現此原則的範例,其中包括:

簡潔性

你的 Go 程式碼,對於使用、閱讀與維護它的人來說都應該是簡潔的。

Go 程式碼應以能達成目標的最簡單方式撰寫,行為與效能皆然。在 Google 的 Go 程式碼庫中,簡潔的程式碼具有下列特徵:

  • 由上而下閱讀都容易理解
  • 不假設讀者已經知道它在做什麼
  • 不假設讀者能記住前面所有的程式碼
  • 不會有多餘的抽象層
  • 命名不會讓人對平凡的事物投入過多注意
  • 讓資料流與決策的傳遞對讀者保持清楚
  • 註解說明的是「為什麼」而不是「做什麼」,以避免日後偏離
  • 文件可獨立成立、不需要外部說明
  • 具有有用的錯誤訊息與有用的測試失敗訊息
  • 通常與「炫技」的寫法是互斥的

「程式碼本身的簡潔」與「API 使用上的簡潔」之間可能會有取捨。例如,有時候讓程式碼複雜一些是值得的,因為這樣 API 的使用者更容易正確地呼叫它;反過來,有時候則值得把一點額外工作留給 API 使用者,以保持程式碼本身的簡單與易懂。

當程式碼確實需要複雜性時,複雜性應該是經過刻意取捨後加入的。通常會在需要額外效能、或某個函式庫/服務有多個截然不同使用者的情境出現。複雜性可能是合理的,但加入時應同時附上文件,讓使用者與未來的維護者能理解並順利探索這些複雜性。最好再補上測試與範例,展示其正確用法,尤其是當程式碼同時存在「簡單寫法」與「複雜寫法」時。

這個原則並不是說 Go 不能或不應該寫複雜程式碼,也不是說 Go 程式碼不允許複雜。我們追求的是一個「不必要的複雜性盡量避免」的程式碼庫,如此一來,當複雜性確實出現時,就代表這段程式碼需要特別小心理解與維護。理想上,旁邊應該有註解說明背後的理由,並指出需要留意的地方。這常常出現在針對效能進行最佳化的程式碼中:這通常需要更複雜的做法,例如預先配置一個緩衝區並在 goroutine 整個生命週期內重複使用。當維護者看到這種寫法時,就應該意識到這段程式碼對效能很敏感,在進行未來變更時就會更加謹慎。反之,如果不必要地引入這種複雜性,反而會成為未來閱讀或修改程式碼者的負擔。

如果某段程式碼最終變得非常複雜,但它的目的本應簡單,這往往是一個訊號:應該回頭重新檢視實作,看看是否有更簡單的方式可以達成相同目標。

最小機制原則

當有多種方式可以表達同一概念時,優先選擇使用最標準工具的那一種。複雜的機制雖然存在,但不應在沒有理由的情況下使用。在程式碼中加入複雜度很容易,但要在事後發現某段複雜度其實多餘並將其移除,卻困難得多。

  1. 若核心語言結構 (例如 channel、slice、map、迴圈、struct) 已能滿足需求,優先使用它。
  2. 若沒有,則找標準函式庫中的工具 (例如 HTTP client 或樣板引擎)。
  3. 最後,在引入新相依套件或自行打造之前,考慮 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 原始碼採用 MixedCapsmixedCaps (即駝峰式命名),而非底線 (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 之前先做一次重構,或選擇至少不會讓區域問題惡化的替代方案。