Go Wiki: Go 程式碼審查評論

本頁面收集了 Go 程式碼審查中常見的評論,以便可以透過簡短的引用來參考詳細的解釋。這是一個常見的風格問題的列表,而不是一個全面的風格指南。

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

有關測試的其他評論可以在 Go 測試評論 中找到。

Google 釋出了更長的 Go 風格指南

請在編輯本頁面之前 討論更改,即使是細微的更改。許多人都有自己的看法,這裡不適合進行編輯戰。

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) { ...

依此類推。

上下文

context.Context 型別的值在 API 和程序邊界之間傳遞安全憑據、跟蹤資訊、截止日期和取消訊號。Go 程式透過完整的函式呼叫鏈顯式地傳遞 Context,從傳入的 RPC 和 HTTP 請求到傳出的請求。

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

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

一個永遠不會特定於請求的函式可以使用 context.Background(),但即使您認為不需要,也要傾向於傳遞一個 Context。預設情況下是傳遞 Context;只有當您有充分的理由證明其他方法是錯誤的,才直接使用 context.Background()

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

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

如果您有應用程式資料需要傳遞,請將其放入引數、接收者、全域性變數,或者(如果它確實屬於那裡)放入 Context 值中。

Context 是不可變的,因此將相同的 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、零長度切片,因為這可能導致微妙的程式設計錯誤。

有關 nil 在 Go 中的更多討論,請參閱 Francesc Campoy 的演講 Understanding Nil

文件註釋

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

不要恐慌

請參閱 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 生命週期

當你生成 goroutine 時,要清楚它們何時 - 或是否 - 退出。

Goroutine 可能因為阻塞在 channel 傳送或接收上而洩露:垃圾收集器不會終止 goroutine,即使它們阻塞的 channel 是不可達的。

即使 goroutine 不洩露,在它們不再需要時讓它們處於執行狀態也會導致其他微妙且難以診斷的問題。傳送到已關閉的 channel 會引發 panic。在“不再需要結果後”修改仍在使用的輸入仍然可能導致資料競爭。並且讓 goroutine 無限期地處於執行狀態可能導致不可預測的記憶體使用。

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

處理錯誤

請參閱 https://golang.org.tw/doc/effective_go#errors。不要使用 _ 變數丟棄錯誤。如果函式返回錯誤,請檢查它以確保函式成功。處理錯誤、返回錯誤,或者在真正特殊的情況下,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 中,因為它使用了 bar/testutil,而 bar/testutil 匯入了 foo。因此,我們使用“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 對多返回值ir 的支援提供了更好的解決方案。函式不應要求客戶端檢查帶內錯誤值,而應返回一個附加值來指示其其他返回值是否有效。此返回值可以是 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

首字母縮略詞

名稱中是首字母縮略詞或縮寫的詞(例如“URL”或“NATO”)具有一致的大小寫。例如,“URL”應顯示為“URL”或“url”(如“urlPony”或“URLPony”),絕不能是“Url”。例如:ServeHTTP 而不是 ServeHttp。對於具有多個已初始化“詞”的識別符號,例如使用“xmlHTTPRequest”或“XMLHTTPRequest”。

此規則也適用於“ID”,當它縮寫為“identifier”時(幾乎所有情況都不是“ego”、“superego”中的“id”),所以寫“appID”而不是“appId”。

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

介面

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

不要在 API 的實現方定義介面“用於模擬”;相反,設計 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{ … } }

而是返回一個具體型別,讓消費者模擬生產者實現。

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) {}

另一方面,如果一個函式返回兩個或三個相同型別的引數,或者結果的含義從上下文中不清楚,那麼在某些情況下新增名稱可能有用。不要僅僅為了避免在函式內部宣告一個 var 而命名結果引數;這會犧牲一點實現上的簡潔性,但代價是 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)

裸返回對於幾行程式碼的函式是可以的。一旦函式變得中等大小,就要明確你的返回值。推論:僅僅因為命名結果引數就能讓你使用裸返回,這並不值得。文件的清晰度總是比節省一兩行函式程式碼更重要。

最後,在某些情況下,你需要命名一個結果引數,以便在延遲的閉包中更改它。這總是可以的。

裸返回

一個不帶引數的 return 語句返回命名的返回引數。這被稱為“裸”返回。

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

請參閱 命名結果引數

包註釋

包註釋,與所有將由 godoc 顯示的註釋一樣,必須緊鄰包子句出現,中間沒有空行。

// 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 新手來說。如有疑問,請使用指標,但有時值接收者也是有意義的,通常是為了效率,例如對於小型不變的結構體或基本型別的值。一些有用的指導方針:

  • 如果接收者是 map、func 或 chan,請不要使用它們的指標。如果接收者是 slice 並且方法不重新切片或重新分配 slice,請不要使用它的指標。
  • 如果方法需要修改接收者,接收者必須是指標。
  • 如果接收者是包含 sync.Mutex 或類似同步欄位的結構體,則接收者必須是指標以避免複製。
  • 如果接收者是大型結構體或陣列,指標接收者效率更高。大到什麼程度?假設它相當於將所有元素作為引數傳遞給方法。如果這感覺太大了,那麼它對於接收者也太大了。
  • 函式或方法,無論是併發地還是從該方法呼叫時,能否修改接收者?呼叫方法時,值型別會建立接收者的副本,因此外部更新不會應用於此接收者。如果更改必須在原始接收者中可見,則接收者必須是指標。
  • 如果接收者是結構體、陣列或 slice,並且其任何元素是指向可能正在變異的內容的指標,則偏向於指標接收者,因為它會向讀者更清楚地表明意圖。
  • 如果接收者是小型陣列或結構體,它是自然的值型別(例如,類似 time.Time 型別的東西),沒有可變欄位也沒有指標,或者只是一個簡單的基本型別,如 int 或 string,那麼值接收者是有意義的。值接收者可以減少可能產生的垃圾量;如果將值傳遞給值方法,則可以使用棧上副本而不是在堆上分配。(編譯器會嘗試智慧地避免這種分配,但它並不總是能成功。)不要僅為此原因選擇值接收者型別,而未先進行效能分析。
  • 不要混合接收者型別。為所有可用方法選擇指標或結構體型別。
  • 最後,如有疑問,請使用指標接收者。

同步函式

優先選擇同步函式——直接返回結果或在返回前完成所有回撥或 channel 操作的函式——而不是非同步函式。

同步函式將 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,“期望 0,得到 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 的一部分。