Uber Go 語言編碼規范
Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 項目,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 將內部的 Go 風格規范 開源到 GitHub,經過一年的積累和更新,該規范已經初具規模,並受到廣大 Gopher 的關注。本文是該規范的中文版本。本版本會根據原版實時更新。
版本
- 當前更新版本:2019-11-13 版本地址:commit:#71
- 如果您發現任何更新、問題或改進,請隨時 fork 和 PR
- Please feel free to fork and PR if you find any updates, issues or improvement.
目錄
介紹
樣式 (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"。
- 數據指針。如果存儲的數據是指針,則直接存儲。如果存儲的數據是一個值,則存儲指向該值的指針。
如果希望接口方法修改基礎數據,則必須使用指針傳遞。
接收器 (receiver) 與接口
使用值接收器的方法既可以通過值調用,也可以通過指針調用。
例如,
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 的精彩講解。
零值 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
錯誤類型
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可用於提取原始錯誤。
Use fmt.Errorf if the callers do not need to detect or handle that specific error case. -
如果調用者不需要檢測或處理的特定錯誤情況,使用
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 |
|---|---|
|
|
性能
性能方面的特定准則只適用於高頻場景。
優先使用 strconv 而不是 fmt
將原語轉換為字符串或從字符串轉換時,strconv速度比fmt快。
| Bad | Good |
|---|---|
|
|
|
|
避免字符串到字節的轉換
不要反復從固定字符串創建字節 slice。相反,請執行一次轉換並捕獲結果。
| Bad | Good |
|---|---|
|
|
|
|
盡量初始化時指定 Map 容量
在盡可能的情況下,在使用 make() 初始化的時候提供容量信息
make(map[T1]T2, hint)
為 make() 提供容量信息(hint)嘗試在初始化時調整 map 大小,
這減少了在將元素添加到 map 時增長和分配的開銷。
注意,map 不能保證分配 hint 個容量。因此,即使提供了容量,添加元素仍然可以進行分配。
| 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 |
|---|---|
|
|
使用字段名初始化結構體
初始化結構體時,幾乎始終應該指定字段名稱。現在由 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) }
小變量作用域
如果有可能,盡量縮小變量作用范圍。除非它與 減少嵌套的規則沖突。
| 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 = iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)
func printInfo(name string, region Region, status Status)
使用原始字符串字面值,避免轉義
Go 支持使用 原始字符串字面值,也就是 " ` " 來表示原生字符串,在需要轉義的場景下,我們應該盡量使用這種方案來替換。
可以跨越多行並包含引號。使用這些字符串可以避免更難閱讀的手工轉義的字符串。
| Bad | Good |
|---|---|
|
|
初始化 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 |
|---|---|
|
|
還可以參考下面資料:
本文由zshipu.com學習筆記或整理或轉載,如有侵權請聯系,必改之。
