Go語言開發規範
Go 語言編碼規範參看 Uber
Uber 是一家美國矽谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 專案,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 將內部的 Go 風格規範 開源到 GitHub,經過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注。本文是該規範的中文版本。本版本會根據原版實時更新。
版本
- 當前更新版本:2021-04-23 版本地址:commit:#114
- 如果您發現任何更新、問題或改進,請隨時 fork 和 PR
- Please feel free to fork and PR if you find any updates, issues or improvement.
目錄
- uber-go/guide 的中文翻譯
- English
- Uber Go 語言編碼規範
- 版本
- 目錄
- 介紹
- 指導原則
- 效能
- 規範
- 程式設計模式
- Linting
- Stargazers over time
介紹
樣式 (style) 是支配我們程式碼的慣例。術語樣式
有點用詞不當,因為這些約定涵蓋的範圍不限於由 gofmt 替我們處理的原始檔格式。
本指南的目的是通過詳細描述在 Uber 編寫 Go 程式碼的注意事項來管理這種複雜性。這些規則的存在是為了使程式碼庫易於管理,同時仍然允許工程師更有效地使用 Go 語言功能。
該指南最初由 Prashant Varanasi 和 Simon Newton 編寫,目的是使一些同事能快速使用 Go。多年來,該指南已根據其他人的反饋進行了修改。
本文件記錄了我們在 Uber 遵循的 Go 程式碼中的慣用約定。其中許多是 Go 的通用準則,而其他擴充套件準則依賴於下面外部的指南:
所有程式碼都應該通過golint
和go vet
的檢查並無錯誤。我們建議您將編輯器設定為:
- 儲存時執行
goimports
- 執行
golint
和go vet
檢查錯誤
您可以在以下 Go 編輯器工具支援頁面中找到更為詳細的資訊:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
指導原則
指向 interface 的指標
您幾乎不需要指向介面型別的指標。您應該將介面作為值進行傳遞,在這樣的傳遞過程中,實質上傳遞的底層資料仍然可以是指標。
介面實質上在底層用兩個欄位表示:
- 一個指向某些特定型別資訊的指標。您可以將其視為"type"。
- 資料指標。如果儲存的資料是指標,則直接儲存。如果儲存的資料是一個值,則儲存指向該值的指標。
如果希望介面方法修改基礎資料,則必須使用指標傳遞(將物件指標賦值給介面變數)。
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
// f1.f()無法修改底層資料
// f2.f() 可以修改底層資料,給介面變數f2賦值時使用的是物件指標
var f1 F = S1{}
var f2 F = &S2{}
Interface 合理性驗證
在編譯時驗證介面的符合性。這包括:
- 將實現特定介面的匯出型別作為介面API 的一部分進行檢查
- 實現同一介面的(匯出和非匯出)型別屬於實現型別的集合
- 任何違反介面合理性檢查的場景,都會終止編譯,並通知給使用者
補充:上面3條是編譯器對介面的檢查機制,
大體意思是錯誤使用介面會在編譯期報錯.
所以可以利用這個機制讓部分問題在編譯期暴露.
Bad | Good |
---|---|
|
|
如果 *Handler
與 http.Handler
的介面不匹配,
那麼語句 var _ http.Handler = (*Handler)(nil)
將無法編譯通過.
賦值的右邊應該是斷言型別的零值。
對於指標型別(如 *Handler
)、切片和對映,這是 nil
;
對於結構型別,這是空結構。
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
接收器 (receiver) 與介面
使用值接收器的方法既可以通過值呼叫,也可以通過指標呼叫。
帶指標接收器的方法只能通過指標或 addressable values呼叫.
例如,
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
sVals := map[int]S{1: {"A"}}
// 你只能通過值呼叫 Read
sVals[1].Read()
// 這不能編譯通過:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// 通過指標既可以呼叫 Read,也可以呼叫 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")
類似的,即使方法有了值接收器,也同樣可以用指標接收器來滿足介面.
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}
var i F
i = s1Val
i = s1Ptr
i = s2Ptr
// 下面程式碼無法通過編譯。因為 s2Val 是一個值,而 S2 的 f 方法中沒有使用值接收器
// i = s2Val
Effective Go 中有一段關於 pointers vs. values 的精彩講解。
補充:
- 一個型別可以有值接收器方法集和指標接收器方法集
- 值接收器方法集是指標接收器方法集的子集,反之不是
- 規則
- 值物件只可以使用值接收器方法集
- 指標物件可以使用 值接收器方法集 + 指標接收器方法集
- 介面的匹配(或者叫實現)
- 型別實現了介面的所有方法,叫匹配
- 具體的講,要麼是型別的值方法集匹配介面,要麼是指標方法集匹配介面
具體的匹配分兩種:
- 值方法集和介面匹配
- 給介面變數賦值的不管是值還是指標物件,都ok,因為都包含值方法集
- 指標方法集和介面匹配
- 只能將指標物件賦值給介面變數,因為只有指標方法集和介面匹配
- 如果將值物件賦值給介面變數,會在編譯期報錯(會觸發介面合理性檢查機制)
為啥 i = s2Val 會報錯,因為值方法集和介面不匹配.
零值 Mutex 是有效的
零值 sync.Mutex
和 sync.RWMutex
是有效的。所以指向 mutex 的指標基本是不必要的。
Bad | Good |
---|---|
|
|
如果你使用結構體指標,mutex 可以非指標形式作為結構體的組成欄位,或者更好的方式是直接嵌入到結構體中。
如果是私有結構體型別或是要實現 Mutex 介面的型別,我們可以使用嵌入 mutex 的方法:
|
|
為私有型別或需要實現互斥介面的型別嵌入。 | 對於匯出的型別,請使用專用欄位。 |
在邊界處拷貝 Slices 和 Maps
slices 和 maps 包含了指向底層資料的指標,因此在需要複製它們時要特別注意。
接收 Slices 和 Maps
請記住,當 map 或 slice 作為函式引數傳入時,如果您儲存了對它們的引用,則使用者可以對其進行修改。
Bad | Good |
---|---|
|
|
返回 slices 或 maps
同樣,請注意使用者對暴露內部狀態的 map 或 slice 的修改。
Bad | Good |
---|---|
|
|
使用 defer 釋放資源
使用 defer 釋放資源,諸如檔案和鎖。
Bad | Good |
---|---|
|
|
Defer 的開銷非常小,只有在您可以證明函式執行時間處於納秒級的程度時,才應避免這樣做。使用 defer 提升可讀性是值得的,因為使用它們的成本微不足道。尤其適用於那些不僅僅是簡單記憶體訪問的較大的方法,在這些方法中其他計算的資源消耗遠超過 defer
。
Channel 的 size 要麼是 1,要麼是無緩衝的
channel 通常 size 應為 1 或是無緩衝的。預設情況下,channel 是無緩衝的,其 size 為零。任何其他尺寸都必須經過嚴格的審查。我們需要考慮如何確定大小,考慮是什麼阻止了 channel 在高負載下和阻塞寫時的寫入,以及當這種情況發生時系統邏輯有哪些變化。(翻譯解釋:按照原文意思是需要界定通道邊界,競態條件,以及邏輯上下文梳理)
Bad | Good |
---|---|
|
|
列舉從 1 開始
在 Go 中引入列舉的標準方法是宣告一個自定義型別和一個使用了 iota 的 const 組。由於變數的預設值為 0,因此通常應以非零值開頭列舉。
Bad | Good |
---|---|
|
|
在某些情況下,使用零值是有意義的(列舉從零開始),例如,當零值是理想的預設行為時。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
使用 time 處理時間
時間處理很複雜。關於時間的錯誤假設通常包括以下幾點。
- 一天有 24 小時
- 一小時有 60 分鐘
- 一週有七天
- 一年 365 天
- 還有更多
例如,1 表示在一個時間點上加上 24 小時並不總是產生一個新的日曆日。
因此,在處理時間時始終使用 "time"
包,因為它有助於以更安全、更準確的方式處理這些不正確的假設。
使用 time.Time
表達瞬時時間
在處理時間的瞬間時使用 time.Time
,在比較、新增或減去時間時使用 time.Time
中的方法。
Bad | Good |
---|---|
|
|
使用 time.Duration
表達時間段
在處理時間段時使用 time.Duration
.
Bad | Good |
---|---|
|
|
回到第一個例子,在一個時間瞬間加上 24 小時,我們用於新增時間的方法取決於意圖。如果我們想要下一個日曆日(當前天的下一天)的同一個時間點,我們應該使用 Time.AddDate
。但是,如果我們想保證某一時刻比前一時刻晚 24 小時,我們應該使用 Time.Add
。
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
對外部系統使用 time.Time
和 time.Duration
儘可能在與外部系統的互動中使用 time.Duration
和 time.Time
例如 :
-
Command-line 標誌:
flag
通過time.ParseDuration
支援time.Duration
-
JSON:
encoding/json
通過其UnmarshalJSON
method 方法支援將time.Time
編碼為 RFC 3339 字串 -
SQL:
database/sql
支援將DATETIME
或TIMESTAMP
列轉換為time.Time
,如果底層驅動程式支援則返回 -
YAML:
gopkg.in/yaml.v2
支援將time.Time
作為 RFC 3339 字串,並通過time.ParseDuration
支援time.Duration
。
當不能在這些互動中使用 time.Duration
時,請使用 int
或 float64
,並在欄位名稱中包含單位。
例如,由於 encoding/json
不支援 time.Duration
,因此該單位包含在欄位的名稱中。
Bad | Good |
---|---|
|
|
當在這些互動中不能使用 time.Time
時,除非達成一致,否則使用 string
和 RFC 3339 中定義的格式時間戳。預設情況下,Time.UnmarshalText
使用此格式,並可通過 time.RFC3339
在 Time.Format
和 time.Parse
中使用。
儘管這在實踐中並不成問題,但請記住,"time"
包不支援解析閏秒時間戳(8728),也不在計算中考慮閏秒(15190)。如果您比較兩個時間瞬間,則差異將不包括這兩個瞬間之間可能發生的閏秒。
錯誤型別
Go 中有多種宣告錯誤(Error) 的選項:
errors.New
對於簡單靜態字串的錯誤fmt.Errorf
用於格式化的錯誤字串- 實現
Error()
方法的自定義型別 - 用
"pkg/errors".Wrap
的 Wrapped errors
返回錯誤時,請考慮以下因素以確定最佳選擇:
-
這是一個不需要額外資訊的簡單錯誤嗎?如果是這樣,
errors.New
足夠了。 -
客戶需要檢測並處理此錯誤嗎?如果是這樣,則應使用自定義型別並實現該
Error()
方法。 -
您是否正在傳播下游函式返回的錯誤?如果是這樣,請檢視本文後面有關錯誤包裝 section on error wrapping 部分的內容。
-
否則
fmt.Errorf
就可以了。
如果客戶端需要檢測錯誤,並且您已使用建立了一個簡單的錯誤 errors.New
,請使用一個錯誤變數。
Bad | Good |
---|---|
|
|
如果您有可能需要客戶端檢測的錯誤,並且想向其中新增更多資訊(例如,它不是靜態字串),則應使用自定義型別。
Bad | Good |
---|---|
|
|
直接匯出自定義錯誤型別時要小心,因為它們已成為程式包公共 API 的一部分。最好公開匹配器功能以檢查錯誤。
// package foo
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}
func Open(file string) error {
return errNotFound{file: file}
}
// package bar
if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}
錯誤包裝 (Error Wrapping)
一個(函式/方法)呼叫失敗時,有三種主要的錯誤傳播方式:
- 如果沒有要新增的其他上下文,並且您想要維護原始錯誤型別,則返回原始錯誤。
- 新增上下文,使用
"pkg/errors".Wrap
以便錯誤訊息提供更多上下文 ,"pkg/errors".Cause
可用於提取原始錯誤。 - 如果呼叫者不需要檢測或處理的特定錯誤情況,使用
fmt.Errorf
。
建議在可能的地方新增上下文,以使您獲得諸如“呼叫服務 foo:連線被拒絕”之類的更有用的錯誤,而不是諸如“連線被拒絕”之類的模糊錯誤。
在將上下文新增到返回的錯誤時,請避免使用“failed to”之類的短語以保持上下文簡潔,這些短語會陳述明顯的內容,並隨著錯誤在堆疊中的滲透而逐漸堆積:
Bad | Good |
---|---|
|
|
|
|
但是,一旦將錯誤傳送到另一個系統,就應該明確訊息是錯誤訊息(例如使用err
標記,或在日誌中以”Failed”為字首)。
另請參見 Don't just check errors, handle them gracefully. 不要只是檢查錯誤,要優雅地處理錯誤
處理型別斷言失敗
type assertion 的單個返回值形式針對不正確的型別將產生 panic。因此,請始終使用“comma ok”的慣用法。
Bad | Good |
---|---|
|
|
不要 panic
在生產環境中執行的程式碼必須避免出現 panic。panic 是 cascading failures 級聯失敗的主要根源 。如果發生錯誤,該函式必須返回錯誤,並允許呼叫方決定如何處理它。
Bad | Good |
---|---|
|
|
panic/recover 不是錯誤處理策略。僅當發生不可恢復的事情(例如:nil 引用)時,程式才必須 panic。程式初始化是一個例外:程式啟動時應使程式中止的不良情況可能會引起 panic。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
即使在測試程式碼中,也優先使用t.Fatal
或者t.FailNow
而不是 panic 來確保失敗被標記。
Bad | Good |
---|---|
|
|
使用 go.uber.org/atomic
使用 sync/atomic 包的原子操作對原始型別 (int32
, int64
等)進行操作,因為很容易忘記使用原子操作來讀取或修改變數。
go.uber.org/atomic 通過隱藏基礎型別為這些操作增加了型別安全性。此外,它包括一個方便的atomic.Bool
型別。
Bad | Good |
---|---|
|
|
避免可變全域性變數
使用選擇依賴注入方式避免改變全域性變數。
既適用於函式指標又適用於其他值型別
Bad | Good |
---|---|
|
|
|
|
避免在公共結構中嵌入型別
這些嵌入的型別洩漏實現細節、禁止型別演化和模糊的文件。
假設您使用共享的 AbstractList
實現了多種列表型別,請避免在具體的列表實現中嵌入 AbstractList
。
相反,只需手動將方法寫入具體的列表,該列表將委託給抽象列表。
type AbstractList struct {}
// 新增將實體新增到列表中。
func (l *AbstractList) Add(e Entity) {
// ...
}
// 移除從列表中移除實體。
func (l *AbstractList) Remove(e Entity) {
// ...
}
Bad | Good |
---|---|
|
|
Go 允許 型別嵌入 作為繼承和組合之間的折衷。
外部型別獲取嵌入型別的方法的隱式副本。
預設情況下,這些方法委託給嵌入例項的同一方法。
結構還獲得與型別同名的欄位。
所以,如果嵌入的型別是 public,那麼欄位是 public。為了保持向後相容性,外部型別的每個未來版本都必須保留嵌入型別。
很少需要嵌入型別。
這是一種方便,可以幫助您避免編寫冗長的委託方法。
即使嵌入相容的抽象列表 interface,而不是結構體,這將為開發人員提供更大的靈活性來改變未來,但仍然洩露了具體列表使用抽象實現的細節。
Bad | Good |
---|---|
|
|
無論是使用嵌入式結構還是使用嵌入式介面,嵌入式型別都會限制類型的演化.
- 向嵌入式介面新增方法是一個破壞性的改變。
- 刪除嵌入型別是一個破壞性的改變。
- 即使使用滿足相同介面的替代方法替換嵌入型別,也是一個破壞性的改變。
儘管編寫這些委託方法是乏味的,但是額外的工作隱藏了實現細節,留下了更多的更改機會,還消除了在文件中發現完整列表介面的間接性操作。
避免使用內建名稱
Go語言規範language specification 概述了幾個內建的,
不應在Go專案中使用的名稱標識predeclared identifiers。
根據上下文的不同,將這些識別符號作為名稱重複使用,
將在當前作用域(或任何巢狀作用域)中隱藏原始識別符號,或者混淆程式碼。
在最好的情況下,編譯器會報錯;在最壞的情況下,這樣的程式碼可能會引入潛在的、難以恢復的錯誤。
Bad | Good |
---|---|
|
|
|
|
注意,編譯器在使用預先分隔的識別符號時不會生成錯誤,
但是諸如go vet
之類的工具會正確地指出這些和其他情況下的隱式問題。
避免使用 init()
儘可能避免使用init()
。當init()
是不可避免或可取的,程式碼應先嚐試:
- 無論程式環境或呼叫如何,都要完全確定。
- 避免依賴於其他
init()
函式的順序或副作用。雖然init()
順序是明確的,但程式碼可以更改,
因此init()
函式之間的關係可能會使程式碼變得脆弱和容易出錯。 - 避免訪問或操作全域性或環境狀態,如機器資訊、環境變數、工作目錄、程式引數/輸入等。
- 避免
I/O
,包括檔案系統、網路和系統呼叫。
不能滿足這些要求的程式碼可能屬於要作為main()
呼叫的一部分(或程式生命週期中的其他地方),
或者作為main()
本身的一部分寫入。特別是,打算由其他程式使用的庫應該特別注意完全確定性,
而不是執行“init magic”
Bad | Good |
---|---|
|
|
|
|
考慮到上述情況,在某些情況下,init()
可能更可取或是必要的,可能包括:
-
不能表示為單個賦值的複雜表示式。
-
可插入的鉤子,如
database/sql
、編碼型別登錄檔等。 -
對Google Cloud Functions和其他形式的確定性預計算的優化。
追加時優先指定切片容量
追加時優先指定切片容量
在儘可能的情況下,在初始化要追加的切片時為make()
提供一個容量值。
Bad | Good |
---|---|
|
|
|
|
主函式退出方式(Exit)
Go程式使用os.Exit
或者 log.Fatal*
立即退出 (使用panic
不是退出程式的好方法,請 don't panic.)
僅在main()
中呼叫其中一個 os.Exit
或者 log.Fatal*
。所有其他函式應將錯誤返回到訊號失敗中。
Bad | Good |
---|---|
|
|
原則上:退出的具有多種功能的程式存在一些問題:
- 不明顯的控制流:任何函式都可以退出程式,因此很難對控制流進行推理。
- 難以測試:退出程式的函式也將退出呼叫它的測試。這使得函式很難測試,並引入了跳過
go test
尚未執行的其他測試的風險。 - 跳過清理:當函式退出程式時,會跳過已經進入
defer
佇列裡的函式呼叫。這增加了跳過重要清理任務的風險。
一次性退出
如果可能的話,你的main()
函式中最多一次 呼叫 os.Exit
或者log.Fatal
。如果有多個錯誤場景停止程式執行,請將該邏輯放在單獨的函式下並從中返回錯誤。
這會縮短 main()
函式,並將所有關鍵業務邏輯放入一個單獨的、可測試的函式中。
Bad | Good |
---|---|
|
|
效能
效能方面的特定準則只適用於高頻場景。
優先使用 strconv 而不是 fmt
將原語轉換為字串或從字串轉換時,strconv
速度比fmt
快。
Bad | Good |
---|---|
|
|
|
|
避免字串到位元組的轉換
不要反覆從固定字串建立位元組 slice。相反,請執行一次轉換並捕獲結果。
Bad | Good |
---|---|
|
|
|
|
指定容器容量
儘可能指定容器容量,以便為容器預先分配記憶體。這將在新增元素時最小化後續分配(通過複製和調整容器大小)。
指定Map容量提示
在儘可能的情況下,在使用 make()
初始化的時候提供容量資訊
make(map[T1]T2, hint)
向make()
提供容量提示會在初始化時嘗試調整map的大小,這將減少在將元素新增到map時為map重新分配記憶體。
注意,與slices不同。map capacity提示並不保證完全的搶佔式分配,而是用於估計所需的hashmap bucket的數量。
因此,在將元素新增到map時,甚至在指定map容量時,仍可能發生分配。
Bad | Good |
---|---|
|
|
|
|
指定切片容量
在儘可能的情況下,在使用make()
初始化切片時提供容量資訊,特別是在追加切片時。
make([]T, length, capacity)
與maps不同,slice capacity不是一個提示:編譯器將為提供給make()
的slice的容量分配足夠的記憶體,
這意味著後續的append()`操作將導致零分配(直到slice的長度與容量匹配,在此之後,任何append都可能調整大小以容納其他元素)。
Bad | Good |
---|---|
|
|
|
|
規範
一致性
本文中概述的一些標準都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;
但是最重要的是,保持一致.
一致性的程式碼更容易維護、是更合理的、需要更少的學習成本、並且隨著新的約定出現或者出現錯誤後更容易遷移、更新、修復 bug
相反,在一個程式碼庫中包含多個完全不同或衝突的程式碼風格會導致維護成本開銷、不確定性和認知偏差。所有這些都會直接導致速度降低、程式碼審查痛苦、而且增加 bug 數量。
將這些標準應用於程式碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程式通過將多個樣式引入到同一程式碼中,違反了上述關注點。
相似的宣告放在一組
Go 語言支援將相似的宣告放在一個組內。
Bad | Good |
---|---|
|
|
這同樣適用於常量、變數和型別宣告:
Bad | Good |
---|---|
|
|
僅將相關的宣告放在一組。不要將不相關的宣告放在一組。
Bad | Good |
---|---|
|
|
分組使用的位置沒有限制,例如:你可以在函式內部使用它們:
Bad | Good |
---|---|
|
|
import 分組
匯入應該分為兩組:
- 標準庫
- 其他庫
預設情況下,這是 goimports 應用的分組。
Bad | Good |
---|---|
|
|
包名
當命名包時,請按下面規則選擇一個名稱:
- 全部小寫。沒有大寫或下劃線。
- 大多數使用命名匯入的情況下,不需要重新命名。
- 簡短而簡潔。請記住,在每個使用的地方都完整標識了該名稱。
- 不用複數。例如
net/url
,而不是net/urls
。 - 不要用“common”,“util”,“shared”或“lib”。這些是不好的,資訊量不足的名稱。
另請參閱 Package Names 和 Go 包樣式指南.
函式名
我們遵循 Go 社群關於使用 MixedCaps 作為函式名 的約定。有一個例外,為了對相關的測試用例進行分組,函式名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested
.
匯入別名
如果程式包名稱與匯入路徑的最後一個元素不匹配,則必須使用匯入別名。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
在所有其他情況下,除非匯入之間有直接衝突,否則應避免匯入別名。
Bad | Good |
---|---|
|
|
函式分組與順序
- 函式應按粗略的呼叫順序排序。
- 同一檔案中的函式應按接收者分組。
因此,匯出的函式應先出現在檔案中,放在struct
, const
, var
定義的後面。
在定義型別之後,但在接收者的其餘方法之前,可能會出現一個 newXYZ()
/NewXYZ()
由於函式是按接收者分組的,因此普通工具函式應在檔案末尾出現。
Bad | Good |
---|---|
|
|
減少巢狀
程式碼應通過儘可能先處理錯誤情況/特殊情況並儘早返回或繼續迴圈來減少巢狀。減少巢狀多個級別的程式碼的程式碼量。
Bad | Good |
---|---|
|
|
不必要的 else
如果在 if 的兩個分支中都設定了變數,則可以將其替換為單個 if。
Bad | Good |
---|---|
|
|
頂層變數宣告
在頂層,使用標準var
關鍵字。請勿指定型別,除非它與表示式的型別不同。
Bad | Good |
---|---|
|
|
如果表示式的型別與所需的型別不完全匹配,請指定型別。
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F 返回一個 myError 型別的例項,但是我們要 error 型別
對於未匯出的頂層常量和變數,使用_作為字首
在未匯出的頂級vars
和consts
, 前面加上字首_,以使它們在使用時明確表示它們是全域性符號。
例外:未匯出的錯誤值,應以err
開頭。
基本依據:頂級變數和常量具有包範圍作用域。使用通用名稱可能很容易在其他檔案中意外使用錯誤的值。
Bad | Good |
---|---|
|
|
結構體中的嵌入
嵌入式型別(例如 mutex)應位於結構體內的欄位列表的頂部,並且必須有一個空行將嵌入式欄位與常規欄位分隔開。
Bad | Good |
---|---|
|
|
內嵌應該提供切實的好處,比如以語義上合適的方式新增或增強功能。
它應該在對使用者不利影響的情況下完成這項工作(另請參見:避免在公共結構中嵌入型別
Avoid Embedding Types in Public Structs)。
嵌入 不應該:
- 純粹是為了美觀或方便。
- 使外部型別更難構造或使用。
- 影響外部型別的零值。如果外部型別有一個有用的零值,則在嵌入內部型別之後應該仍然有一個有用的零值。
- 作為嵌入內部型別的副作用,從外部型別公開不相關的函式或欄位。
- 公開未匯出的型別。
- 影響外部型別的複製形式。
- 更改外部型別的API或型別語義。
- 嵌入內部型別的非規範形式。
- 公開外部型別的實現詳細資訊。
- 允許使用者觀察或控制型別內部。
- 通過包裝的方式改變內部函式的一般行為,這種包裝方式會給使用者帶來一些意料之外情況。
簡單地說,有意識地和有目的地嵌入。一種很好的測試體驗是,
"是否所有這些匯出的內部方法/欄位都將直接新增到外部型別"
如果答案是some
或no
,不要嵌入內部型別-而是使用欄位。
Bad | Good |
---|---|
|
|
|
|
|
|
使用欄位名初始化結構體
初始化結構體時,應該指定欄位名稱。現在由 go vet
強制執行。
Bad | Good |
---|---|
|
|
例外:如果有 3 個或更少的欄位,則可以在測試表中省略欄位名稱。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
本地變數宣告
如果將變數明確設定為某個值,則應使用短變數宣告形式 (:=
)。
Bad | Good |
---|---|
|
|
但是,在某些情況下,var
使用關鍵字時預設值會更清晰。例如,宣告空切片。
Bad | Good |
---|---|
|
|
nil 是一個有效的 slice
nil
是一個有效的長度為 0 的 slice,這意味著,
-
您不應明確返回長度為零的切片。應該返回
nil
來代替。Bad Good if x == "" { return []int{} }
if x == "" { return nil }
-
要檢查切片是否為空,請始終使用
len(s) == 0
。而非nil
。Bad Good func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
-
零值切片(用
var
宣告的切片)可立即使用,無需呼叫make()
建立。Bad Good nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
記住,雖然nil切片是有效的切片,但它不等於長度為0的切片(一個為nil,另一個不是),並且在不同的情況下(例如序列化),這兩個切片的處理方式可能不同。
縮小變數作用域
如果有可能,儘量縮小變數作用範圍。除非它與 減少巢狀的規則衝突。
Bad | Good |
---|---|
|
|
如果需要在 if 之外使用函式呼叫的結果,則不應嘗試縮小範圍。
Bad | Good |
---|---|
|
|
避免參數語義不明確(Avoid Naked Parameters)
函式呼叫中的意義不明確的引數
可能會損害可讀性。當引數名稱的含義不明顯時,請為引數新增 C 樣式註釋 (/* ... */
)
Bad | Good |
---|---|
|
|
對於上面的示例程式碼,還有一種更好的處理方式是將上面的 bool
型別換成自定義型別。將來,該引數可以支援不僅僅侷限於兩個狀態(true/false)。
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status= iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)
func printInfo(name string, region Region, status Status)
使用原始字串字面值,避免轉義
Go 支援使用 原始字串字面值,也就是 " ` " 來表示原生字串,在需要轉義的場景下,我們應該儘量使用這種方案來替換。
可以跨越多行幷包含引號。使用這些字串可以避免更難閱讀的手工轉義的字串。
Bad | Good |
---|---|
|
|
初始化結構體
使用欄位名初始化結構
初始化結構時,幾乎應該始終指定欄位名。目前由go vet
強制執行。
Bad | Good |
---|---|
|
|
例外:當有3個或更少的欄位時,測試表中的欄位名may可以省略。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
省略結構中的零值欄位
初始化具有欄位名的結構時,除非提供有意義的上下文,否則忽略值為零的欄位。
也就是,讓我們自動將這些設定為零值
Bad | Good |
---|---|
|
|
這有助於通過省略該上下文中的預設值來減少閱讀的障礙。只指定有意義的值。
在欄位名提供有意義上下文的地方包含零值。例如,表驅動測試 中的測試用例可以受益於欄位的名稱,即使它們是零值的。
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
對零值結構使用 var
如果在宣告中省略了結構的所有欄位,請使用 var
宣告結構。
Bad | Good |
---|---|
|
|
這將零值結構與那些具有類似於為[初始化 Maps]建立的,區別於非零值欄位的結構區分開來,
並與我們更喜歡的declare empty slices方式相匹配。
初始化 Struct 引用
在初始化結構引用時,請使用&T{}
代替new(T)
,以使其與結構體初始化一致。
Bad | Good |
---|---|
|
|
初始化 Maps
對於空 map 請使用 make(..)
初始化, 並且 map 是通過程式設計方式填充的。
這使得 map 初始化在表現上不同於宣告,並且它還可以方便地在 make 後新增大小提示。
Bad | Good |
---|---|
|
|
宣告和初始化看起來非常相似的。 |
宣告和初始化看起來差別非常大。 |
在儘可能的情況下,請在初始化時提供 map 容量大小,詳細請看 指定Map容量提示。
另外,如果 map 包含固定的元素列表,則使用 map literals(map 初始化列表) 初始化對映。
Bad | Good |
---|---|
|
|
基本準則是:在初始化時使用 map 初始化列表 來新增一組固定的元素。否則使用 make
(如果可以,請儘量指定 map 容量)。
字串 string format
如果你在函式外宣告Printf
-style 函式的格式字串,請將其設定為const
常量。
這有助於go vet
對格式字串執行靜態分析。
Bad | Good |
---|---|
|
|
命名 Printf 樣式的函式
宣告Printf
-style 函式時,請確保go vet
可以檢測到它並檢查格式字串。
這意味著您應儘可能使用預定義的Printf
-style 函式名稱。go vet
將預設檢查這些。有關更多資訊,請參見 Printf 系列。
如果不能使用預定義的名稱,請以 f 結束選擇的名稱:Wrapf
,而不是Wrap
。go vet
可以要求檢查特定的 Printf 樣式名稱,但名稱必須以f
結尾。
$ go vet -printfuncs=wrapf,statusf
另請參閱 go vet: Printf family check.
程式設計模式
表驅動測試
當測試邏輯是重複的時候,通過 subtests 使用 table 驅動的方式編寫 case 程式碼看上去會更簡潔。
Bad | Good |
---|---|
|
|
很明顯,使用 test table 的方式在程式碼邏輯擴充套件的時候,比如新增 test case,都會顯得更加的清晰。
我們遵循這樣的約定:將結構體切片稱為tests
。 每個測試用例稱為tt
。此外,我們鼓勵使用give
和want
字首說明每個測試用例的輸入和輸出值。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
功能選項
功能選項是一種模式,您可以在其中宣告一個不透明 Option 型別,該型別在某些內部結構中記錄資訊。您接受這些選項的可變編號,並根據內部結構上的選項記錄的全部資訊採取行動。
將此模式用於您需要擴充套件的建構函式和其他公共 API 中的可選引數,尤其是在這些功能上已經具有三個或更多引數的情況下。
Bad | Good |
---|---|
|
|
必須始終提供快取和記錄器引數,即使使用者希望使用預設值。
|
只有在需要時才提供選項。
|
Our suggested way of implementing this pattern is with an Option
interface
that holds an unexported method, recording options on an unexported options
struct.
我們建議實現此模式的方法是使用一個 Option
介面,該介面儲存一個未匯出的方法,在一個未匯出的 options
結構上記錄選項。
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
注意: 還有一種使用閉包實現這個模式的方法,但是我們相信上面的模式為作者提供了更多的靈活性,並且更容易對使用者進行除錯和測試。特別是,在不可能進行比較的情況下它允許在測試和模擬中對選項進行比較。此外,它還允許選項實現其他介面,包括 fmt.Stringer
,允許使用者讀取選項的字串表示形式。
還可以參考下面資料:
Linting
比任何 "blessed" linter 集更重要的是,lint在一個程式碼庫中始終保持一致。
我們建議至少使用以下linters,因為我認為它們有助於發現最常見的問題,並在不需要規定的情況下為程式碼質量建立一個高標準:
-
errcheck 以確保錯誤得到處理
-
goimports 格式化程式碼和管理 imports
-
golint 指出常見的文體錯誤
-
govet 分析程式碼中的常見錯誤
-
staticcheck 各種靜態分析檢查
Lint Runners
我們推薦 golangci-lint 作為go-to lint的執行程式,這主要是因為它在較大的程式碼庫中的效能以及能夠同時配置和使用許多規範。這個repo有一個示例配置檔案.golangci.yml和推薦的linter設定。
golangci-lint 有various-linters可供使用。建議將上述linters作為基本set,我們鼓勵團隊新增對他們的專案有意義的任何附加linters。
Stargazers over time