開始評分量表

本文件列出在 Fuchsia 來源樹狀結構中編寫 Go 時應遵循的慣例。這些慣例結合了最佳做法、專案偏好以及為求一致性所做的選擇。

本文所述的慣例可視為新增或修訂常見的最佳做法,詳情請參閱有效的 GoGo 程式碼審查註解

一般

檔案中的宣告順序

在 Go 檔案中宣告的組織方式明顯不同,特別是因為經常一個含有全部內容的大型檔案,或一個包含各方面多個的小型檔案。這裡有幾項經驗法則。

對於頂層宣告 (常數、介面、類型及其相關方法) 的排序是很常見的做法,按照功能群組列出,每個群組中的下列順序如下:

  • 常數 (包含列舉);
  • 匯出的介面、類型和函式。
  • 支援匯出類型的未匯出介面、類型和函式。

舉例來說,fidlgen lib 中的一組功能可能是處理名稱,而另一個群組則可能讀取 JSON IR 並進行去標準化。這類群組在成長時,適合當做自己的個別檔案。

對於採用方法的型別,通常會依序列出:

  • 類型宣告 (例如 type myImpl struct { ...);
  • 類型斷言(如有);
  • 方法,優先使用匯出方法,其次為未匯出的方法;
  • 建議您視情況將實作介面的方法分組,例如將 Foo 介面的所有方法,然後分組為 Bar 介面的所有方法。
  • 一般而言,即使已匯出,String() 方法仍會持續。

如需列舉資訊,請參閱這篇文章,瞭解如何定義列舉。

命名慣例

Go 對於如何命名事物有很高的評價。以下是一些慣例:

  • var err error
  • var buf bytes.Buffer
  • var buf strings.Builder
  • var mu sync.Mutex
  • var wg sync.WaitGroup
  • var ctx context.Context
  • _, ok := someMap[someKey]
  • _, ok := someValue.(someType)

定義方法時,請確保接收器名稱一致 (如果有的話)。另請參閱「指標與數值」。

避免匯出

在 Go 中,您可以將 ID 移出或匯出 (不會使用公開/私人術語)。匯出的宣告以大寫字母 MyImportantThing 開頭,未匯出的宣告則以小寫英文字母 myLocalThing 開頭。

請只匯出您確實需要以及想要重複使用的內容。請注意,後續的變更相對容易匯出,因此建議等到有必要後再匯出。這麼做可讓說明文件更加簡潔。

如果類型會使用反射來使用 (例如在範本或自動差異 go-cmp),您可以在匯出的欄位中有一個未匯出的 ID。再次提醒您,除非是必須這麼做,否則最好不要匯出未匯出的欄位。

本機變數與類型的名稱相同,並無任何問題。如果您為匯出類型編寫 server := Server{...},則 server := server{...} 適用於未匯出的類型。

不得使用已命名的傳回值

語意令人困惑,而且容易出錯。即使在某個階段可能非常實用 (很少使用且懷疑),但演變通常會導致這種情況不再發生,進而造成差異大於實際變化。

您會看到「我們可以針對這個特殊情況使用具名回傳值」或「這個特殊情況適合用於已命名的回傳值」的建議。我們的規則比較簡單,切勿使用具名的傳回值。

(在定義介面時,您可以為事物命名,這與實作中的已命名回傳不同,沒有語意影響。)

定義列舉

定義列舉的標準模式如下:

type myEnumType int

const (
    _ myEnumType = iota
    myFirstEnumValue
    mySecondEnumValue
)

只有在使用 iota 時,才能保留類型。如果使用明確值,您必須重複類型:

const (
    _ myEnumType = iota
    myFirstEnumValue
    mySecondEnumValue
    // ...
    myCombinedValue myEnumType = 100
)

此外,如果您希望此列舉以文字呈現:

var myEnumTypeStrings = map[myEnumType]string{
    myFirstEnumValue:  "myFirstEnumValue",
    mySecondEnumValue: "mySecondEnumValue",
}

func (val myEnumType) String() string {
    if fmt, ok := myEnumTypeStrings[val]; ok {
        return fmt
    }
    return fmt.Sprintf("myEnumType(%d)", val)
}

例如 mdlint: TokenKind。比起使用 switch,使用 map 是比較常見的做法,因為通常會改善程式碼,以便計算地圖以外的內容,例如 FromString 建構函式 (搜尋效率不佳,或預先計算 stringsToMyEnum 反向地圖)。

您應使用值為 0 的 enum 成員,且該成員為自然預設值。詳情請參閱列舉 FIDL API Rubric 項目

另一種做法是使用 cmd/stringer 產生列舉值。這樣會產生更有效率的程式碼,但需要多費工夫維護。當列舉項目處於穩定狀態時,您可以使用此方法。

集合

如要表示集合,請使用對應為空白的結構:

allMembers := map[uint32]struct{}{
    5: {},
    8: {},
}

建立 Slice

如要使用常值定義切片,請使用 []T{ ... } 定義。

否則,請使用 var slice []T 建立空白的配量,也就是請勿使用 slice:= []T{}

請注意,預先分配的配量可帶來顯著的效能提升,而且建議優先於使用較簡單的語法。請參閱配量用量與內部資源的相關說明。

建立地圖的例項

如要使用常值定義地圖,不妨使用 map[K]T{ ... }

否則,請使用 make(map[string]struct{}) 建立空白地圖,也就是不使用 myEmptyMap := map[K]T{}

非一般退貨

當函式需要傳達非正常的回傳 (例如「失敗」) 時,有幾個模式:

  1. func doSomething(...) (data, bool):也就是傳回資料,或傳回 false。
  2. func doSomething(...) *data 例如傳回資料,或傳回 nil。
  3. func doSomething(...) (data, error),例如傳回資料或錯誤。
  4. func doSomething(...) dataWrapper:例如回傳包裝函式,用來包含作業結果的相關結構化資訊。

針對無法採用哪種口種版本的情況提供硬性且快速的規則,但仍有適用的幾項通用原則。

在多型態結構定義中使用 nil 與現在的差異,皆很容易在 Go 中出錯:nil 不是單位,會很多 nil,且彼此不相等!因此,在多型態的結構定義中使用方法時,會優先傳回額外的 ok 布林值。只有在該方法只會用於單態結構定義下時,才使用 nil 或做為指標。或者,也有另一種方式,使用模式 (2) 比 (1) 簡單;此外,如果所有呼叫端都會使用函式,而不透過介面鏡頭查看傳回的值,不妨使用這種模式。

傳回 error 表示發生問題,系統會為呼叫端提供操作的說明。呼叫端預期會顯示錯誤、在過程中收尾,或執行一些錯誤處理和復原作業。與傳回 ok 布林值 (或 nil 資料) 的方法之間的主要差異在於,傳回 error 時,這個方法會斷言自己有足夠的背景資訊來分類運作異常。

舉例來說,如果沒有找到任何使用者,LookupUser(user_id uint64) 會偏好傳回返回布林值,但 LookupCountryCode(code IsoCountryCode) 會優先傳回 error,以便在無法查詢國家/地區 (或要求的國家/地區無效) 時,識別設定錯誤。

如果方法的結果相當複雜,且需要用結構化資料說明,則應傳回某些包裝函式。舉例來說,要使用資料包裝函式的自然候選項目是一種驗證 API,它會掃遍 XML 文件並傳回錯誤路徑清單,每個路徑都會附加警告或錯誤。

靜態類型斷言

由於 Go 會使用結構子類型,也就是是否實作介面要由其結構 (而非宣告) 決定。很容易誤以為型別實作了某些介面,但實際上卻不行。這會導致距離使用網站到處不遠,並造成編譯器混淆。

如要解決這個問題,您必須針對已實作類型的所有介面編寫類型斷言 (是標準程式庫中的類型宣告):

var _ MyInterface = (*myImplementation)(nil)

這項操作會建立型別 nil,其指派作業會強制編譯器檢查類型一致性。

我們通常會有許多實作需要實作相同的介面,例如代表具有個別節點的抽象語法樹狀結構,全都實作運算式介面。在這些情況下,您應該一次執行所有類型斷言,因此也會記錄預期的所有子類型:

var _ = []MyInterface{
    (*myImplementation)(nil),
    (*myOtherImplementation)(nil),
    ...
}

關於應放置這些類型斷言的位置,請參考以下經驗法則:

  • 偏好每項實作各自獨立時 (例如這裡),偏好在實作項目下方使用單一類型斷言。
  • 如果所有實作項目都應用於一起使用 (例如代表 AST 的 Expression 介面運算式節點),請優先選擇介面下方的分組類型斷言,例如這裡

嵌入

嵌入是 Go 中非常強大的概念,善加利用。如需簡介,請參閱嵌入一文。

嵌入介面或結構類型時,應先將其列在其封閉介面或結構類型中,也就是嵌入的類型應顯示為第一個欄位。

指標與值

針對方法接收器,請參閱指標與值接收器類型。tl;dr 為保持一致性,但如有疑慮,請使用指標接收器。針對特定類型,請確保其在傳遞方式方面一律一致,例如一律透過值傳遞、一律透過參照傳遞,而且此流程來自是否在此類型 (含值或指標接收器) 定義方法。

同時請注意,透過值傳遞結構體,會認為呼叫端不會改變其是不正確的。您可以輕鬆地保留地圖、切片或物件的參照,進而變更這些物件。因此在 Go 中認為「pass by value is const」的關聯不正確

如需有關方法接收器的具體建議,請參閱實作介面

實作介面

一般來說,請使用指標接收器實作介面;使用值接收器實作介面會導致該介面同時由值和指標實作,這樣會導致嘗試列舉介面可能實作的類型斷言複雜性。請參閱執行個體 fxrev.dev/269371

以下幾個特定情況適合使用值接收器實作介面:

  • 從未將型別做為指標使用時。舉例來說,您通常可透過定義 type mySlice []myElement 並在 mySlice 上實作 sort.Interface,完成自訂排序。系統一律不使用 *mySlice 類型,因為 []myElement 已是參照項目。請參閱這裡的範例。
  • 當不應使用類型斷言或介面類型值類型切換時,就會發生這種情況。舉例來說,Stringer 通常會在值類型上實作。如果函式接受 val Stringer 切換至 val.(type),是不尋常的函式。

如有疑慮,請一律使用指標接收器實作介面。

留言

請參閱評論,尤其是:

文件註解最適合做為完整句子,可供各種自動化簡報使用。第一句應為以宣告名稱開頭的一句摘要。

// Compile parses a regular expression and returns, if successful,
// a Regexp that can be used to match against text.
func Compile(str string) (*Regexp, error) {

類型的文件註解可能含有前置文章,例如「A」、「An」或「The」。Go 標準程式庫中的所有說明文件都會遵循這項做法,例如 // A Buffer is a variable-sized ...

針對超過句子的註解,樣式通常會優先使用一個摘要句子,然後是空白行,然後是提供詳細詳細資料的段落。請參閱 FileHeader 結構。

包裝錯誤

使用 fmt.Errorf 傳播錯誤時:

  • 使用 %s 即可只加入字串值;
  • 使用 %w 可讓呼叫端解除包裝並觀察已包裝錯誤;請注意,%w 會將這些已包裝的錯誤加入 API 中。

請參閱「在 Go 1.13 中處理錯誤」,以及更具體的「是否包裝」一節。

某些情況下,必須透過符合 API 合約的方式執行錯誤傳播,例如 RDBMS 驅動程式中常見的情況,即傳回的特定錯誤代碼代表呼叫端可能復原的情況。在這種情況下,必須明確包裝潛在錯誤,而非依賴 fmt.Errorf

fmt 個動詞

請盡量避免使用 %v,請優先採用運算元支援的特定 fmt 動詞。這種做法的好處是讓 go vet 能夠確認運算元是否確實支援動詞。

如果運算元是未實作 fmt.Stringer 的結構體,%v 就不太可能產生良好的結果;%+v%#v 可能是更好的選擇。

這項規則的常見例外狀況是運算元在執行階段期間可能為 nil - nil 值由 %v 妥善處理,而非所有其他動詞。

引用字串時,請使用 %q,不要明確呼叫 strconv.Quote

傳播錯誤時,請參閱錯誤包裝

GN 目標

Go 工具的一般 BUILD.gn 檔案如下所示:

go_library("gopkg") {
  sources = [
    "main.go",
    "main_test.go",
  ]
}

go_binary("foo") {
  library = ":gopkg"
}

go_test("foo_test") {
  library = ":gopkg"
}

如果您有巢狀套件 (且僅適用於這個情況),請使用 go_library 中的 name = "go.fuchsia.dev/fuchsia/<path>/..." 表單啟用遞迴套件來源:

go_library("gopkg") {
  name = "go.fuchsia.dev/fuchsia/tools/foo/..."
  sources = [
    "main.go",
    "subdir/bar.go",
    "extra/baz.go",
  ]
}

測試

結尾為 _test 的套件

一般來說,_test.go 檔案位於與測試程式碼相同的套件中 (例如 package foo),而且可以存取未匯出的宣告。Go 也允許您在套件名稱字尾加上 _test (例如 package foo_test),在這種情況下,系統會將其編譯為獨立套件,但連結並執行主要二進位檔。這個方法稱為外部測試,而非內部測試。除非您要編寫外部測試,否則請勿使用含有 _test 後置字串的套件命名,請改為參閱測試套件

在執行整合層級測試時,建議採用外部測試,或是與測試中套件的匯出部分互動。

使用結尾為 _test 的套件也很有趣,可提供已編譯的範例程式碼,您可以透過套件選取器直接複製貼上程式碼。例如範例程式碼來源

測試公用程式

測試公用程式是套件中使用的輔助程式,有助於進行測試。作用是測試程式碼,它位於具有 _test.go 後置字串的檔案中。在名為 testutils_test.go 的檔案中加入測試公用程式;這項慣例被編譯器解釋,使此程式碼不會納入非測試二進位檔中,以確保不會將程式碼用於測試以外的用途。

測試套件

測試套件是一種程式庫,旨在簡化編寫測試。是「實際工作環境代碼」— 結尾不是 _test.go 後置字串,但僅供透過測試程式碼使用。

測試套件的命名慣例是使用「test」字尾以及用於測試的套件名稱。標準程式庫的範例包括 httptestiotest、Fususia 樹狀結構中的 fidlgentest,以及模擬器

其他資源