Go 語言程式碼風格規範-指南篇

張哥說技術發表於2022-12-26

每門開發語言都會有其特有的風格規範(亦或指南),開發者遵循規範能帶來顯著收益,有效促進團隊協作、減少 bug 錯誤、降低維護成本等。

Google 開源的 Google Style Guides ()為多種程式語言提供了風格規範,包括 C++、Java、Python、JavaScript 等。在 2022 年 11 月,Go 語言風格規範(go/index)也終於得到開源。

Google 釋出的 Go 語言風格規範一共包括四部分內容。

  • 概述篇:go/index

  • 指南篇:go/guide

  • 決策篇:go/decisions

  • 最佳實踐篇:go/best-practices

如果你所在的團體還未形成一套系統的 Go 風格規範,不妨參考這份指南。

風格原則

這裡列舉了一些總體原則,它們總結了思考如何編寫可讀性 Go 程式碼。以下是根據重要性排序的可讀性程式碼特性。

  • 清晰性:對讀者來說,程式碼的目的和基本原理是清楚的。

  • 簡單性:程式碼以儘可能簡單的方式實現其目標。

  • 簡潔性:程式碼具有高訊雜比。

  • 可維護性:編寫的程式碼易於維護。

  • 一致性:程式碼與更廣泛的 Google 程式碼庫風格一致。

1. 清晰性

可讀性的核心目標是編寫對讀者而言清晰的程式碼。

清晰主要是透過有效的命名、有用的註釋和高效的程式碼組織來實現的。

清晰性應該是從讀者的角度來看,而非程式碼編寫者。程式碼易於閱讀比易於編寫更重要。程式碼清晰性有兩個不同的方面:

  • 程式碼實際上在做什麼?

  • 為什麼程式碼會做它所做的事情?

程式碼實際上在做什麼?

Go 的設計使得它應該相對直接地看到程式碼在做什麼。在存在不確定或者讀者需要先驗知識才能理解程式碼的情況下,這值得花時間讓未來的讀者更清晰地瞭解程式碼的用途。例如,以下措施可能會有助於提供更清晰的程式碼:

  • 使用更具描述性的變數名

  • 新增附加註釋

  • 用空格和註釋分解程式碼

  • 將程式碼重構為單獨的函式/方法,使其更加模組化

這裡沒有放之四海而皆準的方法,但在開發 Go 程式碼時優先考慮清晰性很重要。

為什麼程式碼會做它所做的事情?

程式碼的基本原理通常透過變數、函式、方法或包的名稱充分傳達。如果沒有,新增註釋很重要。當程式碼包含了讀者可能不熟悉的細微差別時,解釋”為什麼“就顯得尤為重要。例如:

  • 語言上的細微差別,例如,閉包要獲取一個迴圈變數,但是程式碼寫在很多行之外。

  • 業務邏輯的細微差別,例如,需要區分實際使用者和冒充使用者的訪問控制檢查。

某個 API 可能需要額外注意才能正確使用。例如,出於效能原因,一段程式碼可能錯綜複雜且難以理解,或者一系列複雜的數學運算可能以意想不到的方式使用型別轉換。在這些場景以及更多類似的情況下,重要的是要有隨附的註釋和文件來解釋它們,這樣以後的維護者才不會犯錯誤,這些讀者能夠不用進行逆向工程而理解程式碼。

同樣重要的是要注意到,一些為了提供清晰性的嘗試(例如新增額外的註釋)可能會模糊程式碼的實際目的,例如增加混亂、重複描述程式碼、與程式碼自相矛盾的註釋,或者為了保證註釋最新而增加維護負擔。讓程式碼自己說話(例如,透過使用可自描述的符號名稱)而不是新增多餘的註釋。註釋通常最好是解釋為什麼做某事,而不是程式碼在做什麼。

Google 程式碼庫在很大程度上是統一與一致的。通常情況下,與眾不同的程式碼(例如,透過使用不熟悉的模式)那樣做是有充分理由的,通常是為了效能考慮。保持這一特性很重要,它可以讓讀者清楚地知道在閱讀一段新程式碼時他們應該把注意力集中在什麼地方。。

標準庫包含許多實踐該原則的示例。例如其中:

  • sort 包中的維護者註釋。

  • 同樣在 sort 包中有一些很好且可執行的例子,它們同時有利於使用者(例子在[godoc](sort package - sort - Go Packages)顯示)和維護者(作為部分測試用途的例子)。

  • strings.Cut 雖然只有四行程式碼,但它們提高了對呼叫者而言,使用時的清晰性和正確性。

2. 簡單性

對於那些使用、閱讀和維護它的人來說,你的 Go 程式碼應該是簡單的。

Go 程式碼應該以實現其目標的最簡單方式編寫,無論是在行為還是效能方面。在 Google Go 程式碼庫中,簡單的程式碼意味著:

  • 易於從上到下閱讀

  • 不假設你已經提前知道它在做什麼

  • 不假設你可以記住前面的所有程式碼

  • 沒有不必要的抽象層次

  • 沒有引起人們對普通事物的注意的名字

  • 使讀者清楚值和決策的傳播情況

  • 有註釋解釋為什麼而不是什麼,以及程式碼正在做什麼以避免以後的偏差

  • 有獨立的文件

  • 有有用的錯誤和有用的失敗測試用例

  • 可能經常與“故作聰明”的程式碼相互排斥

在程式碼簡單性和 API 使用簡單性之間可能會出現權衡。例如,讓程式碼更復雜可能是值得的,這樣 API 的終端使用者可以更容易且正確地呼叫 API。相比之下,為 API 的終端使用者留一些額外的工作也可能是值得的,這樣程式碼仍然簡單易懂。

當程式碼需要複雜性時,應該刻意地增加複雜性。如果需要額外的效能,或者一個特定的庫或服務有多個不同的使用者,這通常是必要的。複雜性應該是合理的,但它應該隨附文件,以便使用者和未來的維護者能夠理解和駕馭複雜度。這應該輔以證明其正確用法的測試和示例,尤其是在同時存在“簡單”和“複雜”程式碼使用方式的情況下。

我們力求避免程式碼庫中不必要的複雜性,以便當複雜性確實出現時,它表明這些相關程式碼需要仔細理解和維護。理想情況下,應該附有註釋,它解釋基本原理並確定應注意的事項。在最佳化程式碼以提高效能時經常會出現這種情況;這樣做通常需要更復雜的方法,比如預分配緩衝區並在 goroutine 的整個生命週期中重用它。當維護者看到它時,這應該是一個線索,表明相關程式碼是效能的關鍵程式碼,未來對該段程式碼進行修改時應該持有謹慎態度。另一方面,如果使用不當,這種複雜性會給那些將來需要閱讀或更改程式碼的人帶來負擔。

如果程式碼的目的很簡單但最終實現卻很複雜,這通常是應該重新檢視實現以確定是否有更簡單的方式來完成相同目的的訊號。

最少機制

如果有多種方式可以表達相同的想法,請選擇使用最標準的一種。複雜的機制常在,但不應無故使用。根據需要而增加程式碼的複雜性很容易,而在發現不必要的複雜性後,消除現有的複雜性要困難得多。

  1. 在足以滿足你的用例時,使用核心語言結構(例如 slice、map、迴圈或 struct)。

  2. 如果沒有,請在標準庫中尋找一種工具(如 HTTP 客戶端或模板引擎)。

  3. 最後,在引入新的依賴項或自己造輪子之前,請考慮 Google 程式碼庫中是否有對應的核心庫。

例如,考慮包含繫結到具有預設值的變數的標誌的生產環境程式碼,該預設值必須在測試中被覆蓋。除非打算測試程式的命令列介面本身(例如,使用 os/exec),否則直接覆蓋繫結值比使用 flag.Set 更簡單,也更可取。類似地,如果一段程式碼需要對集合成員進行檢查,那麼一個布林值型別的 map(map[string]bool)通常就足夠了。僅當需要更復雜的操作而 map 無法實現或使用起來過於複雜,才應使用提供類集合型別和功能的庫。

3. 簡潔性

簡潔的 Go 程式碼具有很高的訊雜比。應該很容易地辨別相關細節,它透過命名和程式碼結構引導讀者去詳細瞭解。

在任何時候,都會有很多東西阻礙程式碼呈現最主要的細節:

  • 重複程式碼

  • 外來語法

  • 晦澀難懂的名字

  • 不必要的抽象

  • 空格

重複的程式碼模糊了每個幾乎相同部分之間的差異,它需要讀者透過視覺比較相似的程式碼行已找到變化的地方。表格驅動測試室一個很好的機制示例,它可以從每次重複的重要細節中簡明地提取出通用程式碼,但是選擇將哪些部分包含在表格中將影響表格的易懂程度。

在考慮多種方式組織程式碼時,需要考慮哪種方式能讓重要的細節最明顯。

理解和使用常見的程式碼結構和正宗用法對於保持高訊雜比也很重要。例如下面的程式碼塊在錯誤處理中很常見,讀者可以很快理解這塊的用途。

// Good:
if err := doSomething(); err != nil {
    // ...
}

如果程式碼看起來與此非常相似但略有不同,讀者可能不會注意到變化。在這種情況下,值得透過新增註釋以引起注意,來有意“增強”錯誤檢查的訊號。

// Good:
if err := doSomething(); err == nil { // if NO error
    // ...
}

4. 可維護性

程式碼被編輯的次數比它編寫的次數多得多。具有可讀性的程式碼不僅對試圖理解其工作原理的讀者有意義,而且對需要更改它的程式設計師也有意義。清晰性是關鍵。

  • 可維護性的程式碼:

  • 易於被未來的程式設計師正確地修改

  • 具有結構化的 API,以便它們可以優雅地增長

  • 清楚程式碼所做的假設,它選擇對應到問題結構的抽象,而不是對應到程式碼結構

  • 避免不必要的耦合,不包含未使用的功能

  • 有一個全面的測試套件,以確保承諾的行為得到維護,重要的邏輯是正確的,並且測試用例失敗時能提供清晰、可操作的診斷。

當使用像介面和型別這樣的抽象時,根據定義從它們使用的上下文中刪除資訊,重要的是要確保它們提供了足夠的好處。編輯器和 IDE 可以直接連線到方法定義並在使用具體型別時顯示相應的文件,但在其他情況下只能參考對應的介面定義。介面是一個強大的工具,但使用它也有代價,因為維護者可能需要了解底層實現的細節才能正確使用介面,這必須在介面文件或呼叫處進行解釋。

可維護的程式碼也避免了將重要的細節隱藏在容易被忽視的地方。例如,在以下的程式碼例子中,一個字元的存在都會對理解程式碼產生重大的影響。

// 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 
var (
    leap4   = year%4 == 0
    leap100 = year%100 == 0
    leap400 = year%400 == 0
)
leap := leap4 && (!leap100 || leap400)

同樣的,一個隱藏了關鍵邏輯或者重要邊緣情況的輔助函式會很容易地使得之後的更改中可能沒有合適地考慮到它。

可預測的命名是可維護程式碼的另一個特徵。包的使用者或一段程式碼的維護者應該能夠預測給定上下文中的變數、方法或函式的名稱。相同概念的函式引數和接收者通常應該共享相同的名稱,這既可以使文件易於理解,也可以以最小的開銷促進程式碼重構。

可維護程式碼應最小化其依賴性(隱式和顯式)。依賴更少的包意味著可以影響行為的程式碼行更少。避免對內部或者沒有文件記錄的行為的依賴,在將來這些行為發生改變時,這些程式碼就不太可能會成為維護負擔。

在考慮如何組織或編寫程式碼時,值得花時間去思考程式碼可能隨時間的推移而演進的方式。如果給定的方法更有利於未來更簡單、更安全的更改,那通常是一個很好的權衡,即使這意味著設計稍微複雜一些。

5. 一致性

一致的程式碼是在更廣泛的程式碼庫中、在團隊或包的上下文中,甚至在同一個檔案中,看起來、感覺和行為都類似的程式碼。

一致性問題不會凌駕於上述任何原則,但如果必須打破平衡,那麼打破平衡以保持一致性通常是有益的。

包內的一致性通常是最直接重要的一致性級別。如果同一個問題在包中以多種方式處理,或者如果相同概念在一個檔案中有多個名稱,則可能會非常不協調。但是,即使這樣也不應該凌駕於文件化的風格原則或全域性一致性之上。

核心指南

這些指南收集了所有 Go 程式碼都應遵循的 Go 風格最重要的方面。我們希望在編寫可讀性程式碼的時候學習並遵循這些準則。這些準則預計不會被經常更改,並且新新增的內容必須被高標準稽核。

下面的指南擴充套件了 Effective Go 中的建議,這些建議為整個社群的 Go 程式碼提供了一個共同的基線。

格式化

所有 Go 原始檔必須符合 gofmt 工具輸出的格式。此格式由 Google 程式碼庫中的提交前檢查強制執行。生成的程式碼通常也應該格式化(例如,透過使用 format.Source),因為它也可以在程式碼搜尋中瀏覽。

駝峰命名

Go 原始碼在編寫多詞名稱時使用 MixedCaps 或mixedCaps (駝峰命名) 而不是下劃線 (蛇式)。

即使它打破了其他語言的約定,這也適用。例如,常量如果是可匯出的,則為 MaxLength(而非 MAX_LENGTH),如果為未匯出的,則為 maxLength(而非 max_length)。

為了首字母大寫為可匯出的目的,區域性變數被視為未匯出的。

程式碼行長度

Go 原始碼沒有固定的程式碼行長度。如果一行感覺太長,應該考慮重構而不是折斷。如果它已經儘可能短,那麼應該允許該程式碼行保持變長。

不要分割程式碼行的情況:

  • 在縮排更改之前(例如,函式宣告、條件)

  • 使某個長字串(例如 URL)適合多個較短的行

命名

命名與其說是科學,不如說是一門藝術。在 Go 中,名稱往往比許多其他語言短一些,但適用相同的一般準則。命名時應該注意:

  • 使用時不會感到重複

  • 考慮上下文

  • 不再重複已經清楚的概念

你可以在【決策篇】中找到更多關於命名的具體指導。

區域性一致性

在風格指南沒有提及特定風格點的地方,作者可以自由選擇他們喜歡的風格,除非程式碼非常接近的地方(通常在同一個檔案或包中,但有時在團隊或專案目錄中) 在該問題上採取了一致的風格。

有效的區域性風格考量的例子:

  • 使用 %s 或 %v 格式化列印錯誤

  • 使用快取 channel 代替互斥鎖

無效的區域性風格考量的例子:

  • 程式碼行長度限制

  • 使用基於斷言的測試庫

如果區域性風格與風格指南不一致,但可讀性影響僅限於一個檔案,它通常會在程式碼審查中浮出水面,一致的修復將超出相關 CL(change list,變更清單) 的範圍。那時,提交一個 bug 以追蹤該修復是合適的。

如果一個更改會讓現有的風格偏差惡化,在更多的 API 層面被暴露出來,擴大存在偏差的檔案數量,或引入了一個實際的 bug,那麼對於新程式碼而言,區域性一致性不再是違反風格指南的有效理由。在這些情況下,作者應該在同一個 CL 下清理現有的程式碼庫,在當前 CL 之前進行程式碼重構,或者找到一個至少不會使得區域性問題變得更糟的替代方案。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2929461/,如需轉載,請註明出處,否則將追究法律責任。

相關文章