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.
目錄
- 介紹
- 指導原則
- 指向 interface 的指標
- 接收器 (receiver) 與介面
- 零值 Mutex 是有效的
- 在邊界處拷貝 Slices 和 Maps
- 使用 defer 釋放資源
- Channel 的 size 要么是 1,要么是無緩沖的
- 列舉從 1 開始
- 錯誤型別
- 錯誤包裝 (Error Wrapping)
- 處理型別斷言失敗
- 不要 panic
- 使用 go.uber.org/atomic
- 性能
- 優先使用 strconv 而不是 fmt
- 避免字串到位元組的轉換
- 盡量初始化時指定 Map 容量
- 規范
- 一致性
- 相似的宣告放在一組
- import 分組
- 包名
- 函式名
- 匯入別名
- 函式分組與順序
- 減少嵌套
- 不必要的 else
- 頂層變數宣告
- 對于未匯出的頂層常量和變數,使用_作為前綴
- 結構體中的嵌入
- 使用欄位名初始化結構體
- 本地變數宣告
- nil 是一個有效的 slice
- 小變數作用域
- 避免引數語意不明確(Avoid Naked Parameters)
- 使用原始字串字面值,避免轉義
- 初始化 Struct 參考
- 初始化 Maps
- 字串 string format
- 命名 Printf 樣式的函式
- 編程模式
- 表驅動測驗
- 功能選項
介紹
樣式 (style) 是支配我們代碼的慣例,術語樣式有點用詞不當,因為這些約定涵蓋的范圍不限于由 gofmt 替我們處理的源檔案格式,
本指南的目的是通過詳細描述在 Uber 撰寫 Go 代碼的注意事項來管理這種復雜性,這些規則的存在是為了使代碼庫易于管理,同時仍然允許工程師更有效地使用 Go 語言功能,
該指南最初由 Prashant Varanasi 和 Simon Newton 撰寫,目的是使一些同事能快速使用 Go,多年來,該指南已根據其他人的反饋進行了修改,
本檔案記錄了我們在 Uber 遵循的 Go 代碼中的慣用約定,其中許多是 Go 的通用準則,而其他擴展準則依賴于下面外部的指南:
- Effective Go
- The Go common mistakes guide
所有代碼都應該通過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 = https://www.cnblogs.com/sblig/p/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 |
|---|---|
|
|
還可以參考下面資料:
-
Self-referential functions and the design of options
-
Functional options for friendly APIs
本文由zshipu.com學習筆記或整理或轉載,如有侵權請聯系,必改之,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/55292.html
標籤:其他
上一篇:一些用著方便的谷歌插件
