Go Wiki:Go 程式碼評審意見

本頁面收集了 Go 程式碼評審過程中常見的意見,以便可以透過簡寫引用單一詳細解釋。這是一個常見風格問題的清單,並非全面的風格指南。

你可以將此視為 Effective Go 的補充。

與測試相關的其他意見可在 Go Test Comments 找到。

Google 釋出了更長的 Go Style Guide

請在編輯本頁面之前討論變更,即使是細微的修改。許多人有不同意見,這裡不是編輯戰的地方。

Gofmt

對程式碼執行 gofmt 可以自動修復大多數機械式風格問題。幾乎所有實際使用的 Go 程式碼都使用了 gofmt。本文件的其餘部分討論非機械式風格要點。

另一種選擇是使用 goimports,它是 gofmt 的超集,還可以根據需要新增(和刪除)匯入行。

註釋句子

參見 https://golang.org.tw/doc/effective_go#commentary。文件化宣告的註釋應是完整的句子,即使看起來有點多餘。這種方法使得將它們提取到 godoc 文件時格式良好。註釋應以描述物件的名稱開頭,並以句點結尾

// Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

等等。

Contexts

`context.Context` 型別的值在 API 和程序邊界上傳遞安全憑據、跟蹤資訊、截止時間和取消訊號。Go 程式從傳入的 RPC 和 HTTP 請求到傳出的請求,沿著整個函式呼叫鏈顯式傳遞 Contexts。

大多數使用 Context 的函式應將其作為第一個引數接受

func F(ctx context.Context, /* other arguments */) {}

永遠不特定於請求的函式可以使用 context.Background(),但即使你認為不需要,也要傾向於傳遞 Context。預設情況是傳遞 Context;只有當你有一個充分的理由說明另一種選擇是錯誤時,才直接使用 context.Background()。

不要將 Context 成員新增到結構體型別中;相反,在需要傳遞 Context 的該型別上的每個方法中新增一個 ctx 引數。唯一的例外是簽名必須匹配標準庫或第三方庫中介面的方法。

不要建立自定義 Context 型別或在函式簽名中使用 Context 以外的介面。

如果你有需要傳遞的應用資料,請將其放在引數中、接收器中、全域性變數中,或者,如果它確實屬於 Context 值,則放在其中。

Contexts 是不可變的,因此將同一個 ctx 傳遞給共享相同截止時間、取消訊號、憑據、父跟蹤等資訊的多個呼叫是沒問題的。

複製

為避免意外的別名,複製來自另一個包的結構體時要小心。例如,bytes.Buffer 型別包含一個 []byte 切片。如果你複製一個 Buffer,副本中的切片可能會與原始切片中的數組別名,導致後續方法呼叫產生令人驚訝的效果。

一般來說,如果型別 T 的方法與指標型別 *T 相關聯,則不要複製 T 型別的值。

加密隨機數

不要使用 math/randmath/rand/v2 包來生成金鑰,即使是一次性金鑰。用 Time.Nanoseconds() 播種,只有很少的熵。相反,使用 crypto/rand.Reader。如果你需要文字,使用 crypto/rand.Text,或者,用 encoding/hexencoding/base64 對隨機位元組進行編碼。

import (
    "crypto/rand"
    "fmt"
)

func Key() string {
  return rand.Text()
}

宣告空切片

宣告空切片時,首選

var t []string

而不是

t := []string{}

前者宣告一個 nil 切片值,而後者是非 nil 但長度為零的切片。它們功能上等價——它們的 lencap 都為零——但 nil 切片是首選風格。

請注意,在有限的情況下,非 nil 但長度為零的切片是首選,例如編碼 JSON 物件時(nil 切片編碼為 null,而 []string{} 編碼為 JSON 陣列 [])。

設計介面時,避免區分 nil 切片和非 nil、長度為零的切片,因為這可能導致細微的程式設計錯誤。

關於 Go 中 nil 的更多討論,請參閱 Francesc Campoy 的演講 理解 Nil

文件註釋

所有頂級、匯出的名稱都應有文件註釋,非平凡的未匯出型別或函式宣告也應如此。有關注釋約定的更多資訊,請參閱 https://golang.org.tw/doc/effective_go#commentary

不要使用 Panic

參見 https://golang.org.tw/doc/effective_go#errors。不要將 panic 用於常規錯誤處理。使用 error 和多個返回值。

錯誤字串

錯誤字串不應以大寫字母開頭(除非以專有名詞或首字母縮寫詞開頭),也不應以標點符號結尾,因為它們通常在其他上下文之後列印。也就是說,使用 fmt.Errorf("something bad") 而不是 fmt.Errorf("Something bad"),這樣 log.Printf("Reading %s: %v", filename, err) 格式化時訊息中間不會出現不必要的首字母大寫。這不適用於日誌記錄,日誌記錄隱含地按行處理,不會與其他訊息組合。

示例

新增新包時,包括預期用法的示例:可執行的 Example,或一個展示完整呼叫序列的簡單測試。

閱讀更多關於可測試的 Example() 函式的資訊。

Goroutine 生命週期

當你生成 goroutines 時,要清楚它們何時(或是否)退出。

Goroutines 可能因阻塞在通道傳送或接收上而洩漏:即使它阻塞的通道無法訪問,垃圾回收器也不會終止 goroutine。

即使 goroutines 不會洩漏,在不再需要時讓它們繼續執行也可能導致其他細微且難以診斷的問題。向已關閉的通道傳送資料會導致 panic。在“結果不再需要”後修改仍在使用的輸入仍可能導致資料競爭。讓 goroutines 任意長時間執行可能導致不可預測的記憶體使用。

儘量保持併發程式碼足夠簡單,使得 goroutine 的生命週期顯而易見。如果確實不可行,請記錄 goroutines 何時以及為何退出。

處理錯誤

參見 https://golang.org.tw/doc/effective_go#errors。不要使用 _ 變數丟棄錯誤。如果函式返回 error,請檢查它以確保函式成功。處理錯誤,返回它,或者,在真正異常的情況下,使用 panic。

匯入

避免重新命名匯入,除非是為了避免名稱衝突;好的包名不應需要重新命名。發生衝突時,優先重新命名最本地的或專案特定的匯入。

匯入按組組織,組之間有空行。標準庫包總是放在第一組。

package main

import (
    "fmt"
    "hash/adler32"
    "os"

    "github.com/foo/bar"
    "rsc.io/goversion/version"
)

goimports 會為你做這件事。

空白匯入

僅因其副作用而匯入的包(使用語法 import _ "pkg")應僅在程式的 main 包或需要它們的測試中匯入。

點匯入

import . 形式在由於迴圈依賴而無法成為被測試包一部分的測試中可能很有用

package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)

在這種情況下,測試檔案不能放在包 foo 中,因為它使用了匯入了 foo 的 bar/testutil。因此,我們使用 'import .' 形式讓檔案假裝是包 foo 的一部分,即使它不是。除了這種情況,不要在你的程式中使用 import .。它使得程式更難閱讀,因為像 Quux 這樣的名稱不清楚是當前包還是匯入包中的頂級識別符號。

帶內錯誤

在 C 及類似語言中,函式通常返回 -1 或 null 等值來表示錯誤或缺少結果

// Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string

// Failing to check for an in-band error value can lead to bugs:
Parse(Lookup(key))  // returns "parse failure for value" instead of "no value for key"

Go 對多返回值(multiple return values)的支援提供了一個更好的解決方案。函式不應要求客戶端檢查帶內(in-band)錯誤值,而應返回一個額外的值來指示其其他返回值是否有效。該返回值可以是 error,或者在不需要解釋時可以是 boolean。它應是最終的返回值。

// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)

這可以防止呼叫者錯誤地使用結果

Parse(Lookup(key))  // compile-time error

並鼓勵編寫更健壯和可讀的程式碼

value, ok := Lookup(key)
if !ok {
    return fmt.Errorf("no value for %q", key)
}
return Parse(value)

此規則適用於匯出的函式,但對於未匯出的函式也很有用。

當 nil, “”, 0 和 -1 等返回值是函式的有效結果時,它們是可以接受的,也就是說,呼叫者不需要將它們與其他值區別對待。

一些標準庫函式,例如 “strings” 包中的函式,會返回帶內錯誤值。這大大簡化了字串操作程式碼,代價是要求程式設計師更加勤勉。一般來說,Go 程式碼應為錯誤返回額外的值。

錯誤處理流程縮排

嘗試將正常程式碼路徑保持在最小縮排,並將錯誤處理縮排,先處理錯誤。這透過允許快速視覺掃描正常路徑來提高程式碼的可讀性。例如,不要這樣寫

if err != nil {
    // error handling
} else {
    // normal code
}

而是這樣寫

if err != nil {
    // error handling
    return // or continue, etc.
}
// normal code

如果 if 語句有初始化語句,例如

if x, err := f(); err != nil {
    // error handling
    return
} else {
    // use x
}

那麼這可能需要將短變數宣告移到自己的行

x, err := f()
if err != nil {
    // error handling
    return
}
// use x

首字母縮寫

名稱中是首字母縮寫詞(initialisms 或 acronyms)(例如 “URL” 或 “NATO”)的單詞應保持一致的大小寫。例如,“URL” 應顯示為 “URL” 或 “url”(如 “urlPony” 或 “URLPony”),絕不能是 “Url”。舉個例子:ServeHTTP 而不是 ServeHttp。對於包含多個首字母縮寫詞“單詞”的識別符號,例如使用 “xmlHTTPRequest” 或 “XMLHTTPRequest”。

此規則也適用於 “ID”,當它代表“識別符號”(identifier)時(幾乎所有情況,除非是像 “ego”, “superego” 中的 “id”),所以寫 “appID” 而不是 “appId”。

協議緩衝區編譯器生成的程式碼不受此規則約束。人工編寫的程式碼比機器編寫的程式碼有更高的標準。

介面

Go 介面通常屬於使用該介面型別值的包,而不是實現這些值的包。實現包應返回具體型別(通常是指標或結構體):這樣,可以在不進行大量重構的情況下向實現中新增新方法。

不要在 API 的實現者端定義介面用於“mocking”;相反,設計 API,使其可以使用真實實現的公共 API 進行測試。

不要在介面使用之前定義它們:如果沒有實際的使用示例,很難判斷介面是否必要,更不用說它應該包含哪些方法了。

package consumer  // consumer.go

type Thinger interface { Thing() bool }

func Foo(t Thinger) string { … }
package consumer // consumer_test.go

type fakeThinger struct{ … }
func (t fakeThinger) Thing() bool { … }
…
if Foo(fakeThinger{…}) == "x" { … }
// DO NOT DO IT!!!
package producer

type Thinger interface { Thing() bool }

type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }

func NewThinger() Thinger { return defaultThinger{ … } }

相反,返回一個具體型別,讓消費者 mock 生產者實現。

package producer

type Thinger struct{ … }
func (t Thinger) Thing() bool { … }

func NewThinger() Thinger { return Thinger{ … } }

行長度

Go 程式碼沒有嚴格的行長度限制,但要避免過長的行。同樣,如果長行更易讀(例如重複性內容),不要為了縮短行而新增換行符。

大多數時候,當人們“不自然地”換行(例如在函式呼叫或函式宣告中間),如果引數數量合理且變數名足夠短,這些換行是沒有必要的。長行似乎與長名稱相關,去除長名稱非常有幫助。

換句話說,基於你正在編寫內容的語義(作為一般規則)來斷行,而不是因為行的長度。如果你發現這導致行太長,那就改變名稱或語義,你可能會得到一個好結果。

這實際上與函式長度的建議完全相同。沒有“函式永遠不能超過 N 行”這樣的規則,但確實存在函式過長以及過於重複的微小函式的問題,解決方案是改變函式邊界的位置,而不是開始計算行數。

駝峰命名

參見 https://golang.org.tw/doc/effective_go#mixed-caps。即使這打破了其他語言的約定,此規則也適用。例如,未匯出的常量是 maxLength 而不是 MaxLengthMAX_LENGTH

另請參閱 首字母縮寫

命名結果引數

考慮在 godoc 中會是什麼樣子。命名結果引數,例如

func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}

在 godoc 中會顯得重複;最好使用

func (n *Node) Parent1() *Node {}
func (n *Node) Parent2() (*Node, error) {}

另一方面,如果函式返回兩個或三個相同型別的引數,或者結果的含義在上下文中不明確,新增名稱在某些情況下可能有用。不要僅僅為了避免在函式內部宣告變數而命名結果引數;這樣做是以不必要的 API 冗餘為代價換取了微小的實現簡潔性。

func (f *Foo) Location() (float64, float64, error)

不如

// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

如果函式只有幾行,裸返回(Naked returns)是可以接受的。一旦函式變大,請明確指定你的返回值。推論:僅僅因為它能讓你使用裸返回而命名結果引數是不值得的。文件的清晰度永遠比在函式中節省一兩行更重要。

最後,在某些情況下,你需要命名結果引數以便在 deferred closure 中修改它。這總是允許的。

裸返回

不帶引數的 return 語句返回命名結果引數的值。這被稱為“裸返回” (naked return)。

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

參見 命名結果引數

包註釋

包註釋,與所有將在 godoc 中呈現的註釋一樣,必須緊鄰 package 子句,中間沒有空行。

// Package math provides basic constants and mathematical functions.
package math
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template

對於“package main”註釋,在二進位制名稱之後可以使用其他風格的註釋(如果二進位制名稱在前面,可以首字母大寫),例如,對於目錄 seedgen 中的 package main,你可以寫

// Binary seedgen ...
package main

// Command seedgen ...
package main

// Program seedgen ...
package main

// The seedgen command ...
package main

// The seedgen program ...
package main

// Seedgen ..
package main

這些是示例,其合理的變體是可以接受的。

請注意,以小寫字母開頭的單詞作為包註釋的句子開頭是不可接受的選項之一,因為這些註釋是公開可見的,應以規範的英語書寫,包括句子的第一個單詞要大寫。當二進位制名稱是第一個單詞時,即使它與命令列呼叫的拼寫不完全匹配,也需要將其首字母大寫。

有關注釋約定的更多資訊,請參閱 https://golang.org.tw/doc/effective_go#commentary

包名

所有引用你的包中的名稱都將使用包名,因此你可以從識別符號中省略該名稱。例如,如果你在 chubby 包中,你不需要型別 ChubbyFile,客戶端會寫成 chubby.ChubbyFile。相反,將型別命名為 File,客戶端會寫成 chubby.File。避免使用 util, common, misc, api, types 和 interfaces 等無意義的包名。更多資訊請參閱 https://golang.org.tw/doc/effective_go#package-nameshttps://golang.org.tw/blog/package-names

傳遞值

不要僅僅為了節省幾個位元組而將指標作為函式引數傳遞。如果函式始終只以 *x 的形式引用其引數 x,那麼該引數就不應該是指標。這種情況的常見示例包括傳遞字串指標 (*string) 或介面值指標 (*io.Reader)。在這兩種情況下,值本身是固定大小的,可以直接傳遞。此建議不適用於大型結構體,甚至可能增長的小型結構體。

接收器名稱

方法接收器的名稱應反映其身份;通常其型別的單字母或雙字母縮寫就足夠了(例如,“Client” 的“c”或“cl”)。不要使用“me”、“this”或“self”等泛型名稱,這些是面嚮物件語言中賦予方法特殊含義的典型識別符號。在 Go 中,方法的接收器只是另一個引數,因此應相應地命名。該名稱不必像方法引數那樣具有描述性,因為其作用顯而易見且沒有文件目的。它可以非常短,因為它將出現在該型別幾乎每個方法的每一行上;熟悉性允許簡潔。也要保持一致:如果你在一個方法中將接收器稱為“c”,不要在另一個方法中稱其為“cl”。

接收器型別

選擇在方法上使用值接收器還是指標接收器可能很困難,特別是對於 Go 新手程式設計師。如果拿不準,請使用指標,但有時值接收器是合理的,通常出於效率原因,例如對於小型不變的結構體或基本型別的值。一些有用的指導原則

同步函式

優先選擇同步函式(直接返回結果或在返回前完成任何回撥或通道操作的函式)而不是非同步函式。

同步函式將 goroutine 侷限在一次呼叫內部,使其更容易推斷它們的生命週期,並避免洩漏和資料競爭。它們也更容易測試:呼叫者可以傳遞輸入並檢查輸出,無需輪詢或同步。

如果呼叫者需要更多併發性,他們可以透過從單獨的 goroutine 呼叫函式輕鬆新增。但在呼叫者端移除不必要的併發性則相當困難,有時甚至是不可能的。

有用的測試失敗資訊

測試失敗時應提供有用的訊息,說明哪裡出錯了,使用了什麼輸入,實際得到了什麼,以及預期是什麼。編寫一堆 assertFoo 輔助函式可能很誘人,但請確保你的輔助函式能產生有用的錯誤訊息。假設除錯你的失敗測試的人不是你,也不是你的團隊。一個典型的 Go 測試失敗資訊如下所示

if got != tt.want {
    t.Errorf("Foo(%q) = %d; want %d", tt.in, got, tt.want) // or Fatalf, if test can't test anything more past this point
}

請注意,這裡的順序是 actual != expected,並且訊息也使用這個順序。一些測試框架鼓勵反向書寫:0 != x,“expected 0, got x”等等。Go 不是這樣。

如果這看起來需要很多輸入,你可能想寫一個表格驅動測試

使用帶有不同輸入的測試輔助函式時,另一種區分失敗測試的常見技巧是用不同的 TestFoo 函式包裝每個呼叫方,這樣測試失敗時會顯示該名稱

func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T)    { testHelper(t, []int{}) }

無論如何,你有責任向將來除錯你的程式碼的人提供有用的失敗資訊。

變數名

Go 中的變數名應短而不是長。對於作用域有限的區域性變數尤其如此。優先使用 c 而不是 lineCount。優先使用 i 而不是 sliceIndex

基本規則是:名稱的使用位置離其宣告越遠,名稱就必須越具描述性。對於方法接收器,一兩個字母就足夠了。迴圈索引和讀取器等常用變數可以是單個字母(ir)。更不尋常的事物和全域性變數需要更具描述性的名稱。

另請參閱 Google Go 風格指南 中更詳細的討論。


此內容是 Go Wiki 的一部分。