Go Wiki:Go 程式碼檢閱意見
此頁面收集在 Go 程式碼檢閱期間提出的常見意見,以便透過簡寫方式參照單一詳細說明。這是一份常見樣式問題的清單,而非全面的樣式指南。
你可以將這視為 Effective Go 的補充。
與測試相關的額外註解可以在 Go 測試註解 中找到
Google 發布了一份較長的 Go 風格指南。
請在編輯此頁面之前 討論變更,即使是次要的變更。許多人都有意見,這裡不是編輯爭論的地方。
- Gofmt
- 註解句子
- 脈絡
- 複製
- 加密隨機數
- 宣告空切片
- 文件註解
- 不要驚慌
- 錯誤字串
- 範例
- Goroutine 生命週期
- 處理錯誤
- 匯入
- 匯入空白
- 匯入點
- 頻道內錯誤
- 縮排錯誤流程
- 縮寫
- 介面
- 程式碼行長度
- 混合大小寫
- 命名回傳參數
- 裸回傳
- 套件註解
- 套件名稱
- 傳遞值
- 接收器名稱
- 接收器類型
- 同步函式
- 有用的測試失敗
- 變數名稱
Gofmt
在你的程式碼上執行 gofmt 以自動修正大部分的機械式風格問題。幾乎所有通用的 Go 程式碼都使用 gofmt
。本文檔的其餘部分將說明非機械式的風格要點。
另一種選擇是使用 goimports,它是 gofmt
的超集,此外還會根據需要新增(和移除)匯入程式碼行。
註解句子
請參閱 https://go.dev.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 成員新增到結構類型;相反地,請為該類型中需要傳遞 Context 的每個方法新增一個 ctx 參數。唯一的例外是簽章必須與標準函式庫或第三方函式庫中的介面相符的方法。
不要建立自訂 Context 類型或在函式簽章中使用 Context 以外的介面。
如果您有要傳遞的應用程式資料,請將其放入參數、接收器、全域變數中,或者如果它確實屬於那裡,則放入 Context 值中。
Context 是不可變的,因此將相同的 ctx 傳遞給具有相同截止時間、取消訊號、認證、父追蹤等的多個呼叫是沒問題的。
複製
為了避免意外的別名,在從其他套件複製結構時請小心。例如,bytes.Buffer 類型包含一個 []byte
切片。如果您複製一個 Buffer
,複製中的切片可能會別名原始陣列,導致後續的方法呼叫產生令人驚訝的效果。
一般來說,如果值的類型為 T
,而其方法與指標類型 *T
關聯,則不要複製該值。
加密隨機數
不要使用套件 math/rand
來產生金鑰,即使是拋棄式的金鑰。未設定種子的產生器是完全可預測的。使用 time.Nanoseconds()
設定種子時,只有少數位元的熵。請改用 crypto/rand
的 Reader,如果您需要文字,請列印為十六進位或 base64
import (
"crypto/rand"
// "encoding/base64"
// "encoding/hex"
"fmt"
)
func Key() string {
buf := make([]byte, 16)
_, err := rand.Read(buf)
if err != nil {
panic(err) // out of randomness, should never happen
}
return fmt.Sprintf("%x", buf)
// or hex.EncodeToString(buf)
// or base64.StdEncoding.EncodeToString(buf)
}
宣告空切片
在宣告一個空的切片時,優先使用
var t []string
而不是
t := []string{}
前者宣告一個 nil 切片值,而後者是非 nil 但長度為零。它們在功能上是等效的,它們的 len
和 cap
都是零,但 nil 切片是首選的樣式。
請注意,在有限的情況下,優先使用非 nil 但長度為零的切片,例如編碼 JSON 物件時(nil
切片編碼為 null
,而 []string{}
編碼為 JSON 陣列 []
)。
在設計介面時,避免區分 nil 切片和非 nil、長度為零的切片,因為這可能會導致微妙的程式設計錯誤。
有關 Go 中 nil 的更多討論,請參閱 Francesc Campoy 的演講 了解 Nil。
文件註解
所有頂層、匯出的名稱都應該有文件註解,非平凡的未匯出類型或函式宣告也應該有。請參閱 https://go.dev.org.tw/doc/effective_go#commentary 以取得有關註解慣例的更多資訊。
不要驚慌
請參閱 https://go.dev.org.tw/doc/effective_go#errors。不要使用 panic 來處理一般錯誤。請使用錯誤和多個回傳值。
錯誤字串
錯誤字串不應大寫(除非以專有名詞或縮寫開頭)或以標點符號結尾,因為它們通常在其他內容之後列印。也就是說,請使用 fmt.Errorf("something bad")
而不是 fmt.Errorf("Something bad")
,這樣 log.Printf("Reading %s: %v", filename, err)
才能格式化,而訊息中間不會出現多餘的大寫字母。這不適用於記錄,因為記錄隱含地以行為導向,而不是與其他訊息結合在一起。
範例
新增套件時,請包含預期用法的範例:可執行範例,或展示完整呼叫順序的簡單測試。
進一步了解 可測試的 Example() 函式。
Goroutine 生命週期
當您產生 goroutine 時,請清楚說明它們何時(或是否)結束。
goroutine 會因為封鎖在通道傳送或接收上而外洩:即使 goroutine 封鎖的通道無法存取,垃圾回收器也不會終止 goroutine。
即使 goroutine 沒有外洩,在不再需要時讓它們持續執行也可能導致其他難以察覺且難以診斷的問題。傳送至已關閉通道會引發恐慌。修改仍在使用的輸入「在結果不再需要之後」仍可能導致資料競爭。讓 goroutine 持續執行一段任意長的時間可能會導致記憶體使用量無法預測。
請盡量讓並行程式碼保持簡單,以使 goroutine 的生命週期顯而易見。如果這不可行,請記錄 goroutine 何時以及為何結束。
處理錯誤
請參閱 https://go.dev.org.tw/doc/effective_go#errors。請勿使用 _
變數來捨棄錯誤。如果函式傳回錯誤,請檢查函式是否成功。處理錯誤、傳回錯誤,或在真正特殊的情況下引發恐慌。
匯入
避免重新命名匯入,除非要避免名稱衝突;良好的套件名稱不應需要重新命名。如果發生衝突,請優先重新命名最本機或專案特定的匯入。
匯入會以群組整理,群組之間有空白行。標準函式庫套件始終位於第一個群組。
package main
import (
"fmt"
"hash/adler32"
"os"
"github.com/foo/bar"
"rsc.io/goversion/version"
)
goimports 會為您執行此操作。
匯入空白
僅匯入以產生副作用的套件(使用語法 import _ "pkg"
)時,應僅在程式的 main 套件或需要它們的測試中匯入。
匯入點
匯入 . 形式可於測試中使用,由於循環相依性,這些測試無法成為受測套件的一部分
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 支援多個傳回值,提供了更好的解決方案。函式不應要求用戶端檢查頻帶內錯誤值,而應傳回其他值,以表示其其他傳回值是否有效。這個傳回值可以是錯誤,或在不需要說明時為布林值。它應該是最後一個傳回值。
// 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」,當它是「識別碼」的縮寫時(幾乎所有情況都是這樣,當它不是「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://go.dev.org.tw/doc/effective_go#mixed-caps。即使它違反其他語言的慣例,這也適用。例如,未輸出的常數是 maxLength
,而不是 MaxLength
或 MAX_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
對於「封包 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://go.dev.org.tw/doc/effective_go#commentary,以取得有關註解慣例的更多資訊。
套件名稱
封包中所有名稱的參考都將使用封包名稱來完成,因此你可以從識別碼中省略該名稱。例如,如果你在封包 chubby 中,你不需要型別 ChubbyFile,因為客戶端會將其寫成 chubby.ChubbyFile
。相反,將型別命名為 File
,客戶端會將其寫成 chubby.File
。避免使用無意義的封包名稱,例如 util、common、misc、api、types 和 interfaces。請參閱 https://go.dev.org.tw/doc/effective_go#package-names 和 https://go.dev.org.tw/blog/package-names 以取得更多資訊。
傳遞值
不要傳遞指標作為函式引數,只為了節省幾個位元組。如果函式只在整個函式中將其引數 x
參照為 *x
,則引數不應該是指標。這方面的常見範例包括傳遞指標給字串 (*string
) 或傳遞指標給介面值 (*io.Reader
)。在這兩種情況下,值本身都是固定大小,且可以直接傳遞。此建議不適用於大型結構,甚至可能成長的小型結構。
接收器名稱
方法接收者的名稱應反映其身分;通常其類型的一或兩個字母縮寫就已足夠(例如「Client」的「c」或「cl」)。不要使用一般名稱,例如「me」、「this」或「self」,這些識別碼通常用於物件導向語言,會賦予方法特殊意義。在 Go 中,方法的接收者只不過是另一個參數,因此應適當地命名。名稱不需要像方法引數那樣具有描述性,因為其角色很明顯,且不具備文件目的。它可以非常簡短,因為它會出現在該類型的每個方法的幾乎每一行中;熟悉度允許簡潔。也要保持一致:如果你在一個方法中將接收者稱為「c」,就不要在另一個方法中將它稱為「cl」。
接收器類型
選擇在方法中使用值接收者或指標接收者可能會很困難,特別是對新的 Go 程式設計師而言。如有疑問,請使用指標,但有時值接收者才有意義,通常是出於效率的原因,例如對於小型不變結構或基本類型的值。一些有用的準則
- 如果接收者是 map、func 或 chan,請不要對它們使用指標。如果接收者是切片,且方法不會重新切片或重新配置切片,請不要對它使用指標。
- 如果方法需要變異接收器,則接收器必須是一個指標。
- 如果接收器是一個包含 sync.Mutex 或類似同步欄位的結構,則接收器必須是一個指標,以避免複製。
- 如果接收器是一個大型結構或陣列,則指標接收器更有效率。多大的結構才算大?假設它等於將其所有元素作為參數傳遞給方法。如果感覺太大了,那麼對於接收器來說也過大了。
- 函式或方法,無論是同時執行還是從此方法中呼叫,是否可以變異接收器?當呼叫方法時,值類型會建立接收器的副本,因此外部更新不會套用於此接收器。如果變更必須在原始接收器中可見,則接收器必須是一個指標。
- 如果接收器是一個結構、陣列或切片,並且其任何元素都是指向可能變異內容的指標,則優先使用指標接收器,因為它會讓讀者更清楚意圖。
- 如果接收器是一個小型陣列或結構,它本質上是一個值類型(例如,類似 time.Time 類型的東西),沒有可變異欄位和指標,或者只是一個簡單的基本類型,例如 int 或字串,則值接收器是有意義的。值接收器可以減少可以產生的垃圾數量;如果將值傳遞給值方法,則可以使用堆疊上的副本,而不是在堆上配置。 (編譯器會嘗試聰明地避免這種配置,但並非總能成功。)不要因為這個原因而選擇值接收器類型,而沒有先進行設定檔分析。
- 不要混用接收器類型。為所有可用的方法選擇指標或結構類型。
- 最後,有疑問時,請使用指標接收器。
同步函式
優先使用同步函式 - 直接傳回其結果或在傳回之前完成任何回呼或通道操作的函式 - 而不是非同步函式。
同步函式將 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
。
基本規則:名稱離宣告處越遠,名稱就必須越具描述性。對於方法接收器,一或兩個字母就已足夠。常見變數,例如迴圈指標和讀取器,可以使用單一字母 (i
、r
)。較不常見的事物和全域變數需要使用更具描述性的名稱。
另請參閱 Google Go 風格指南 中的較長討論。
此內容是 Go Wiki 的一部分。