uber-go guide,uber的go語言編碼規范
感謝翻譯者和原作們
本文轉自:https://github.com/xxjwxc/uber_go_guide_cn
(特此感謝作者的翻譯,感謝他們的辛苦付出)
原英文:https://github.com/uber-go/guide,uber-go/guide 的中文翻譯
原英文:uber-go style
(也感謝uber-go的原作者們)
uber-go/guide 的中文翻譯
English
Uber Go 語言編碼規范
Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 項目,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年末 Uber 將內部的 Go 風格規范 開源到 GitHub,經過一年的積累和更新,該規范已經初具規模,並受到廣大 Gopher 的關注。本文是該規范的中文版本。本版本會根據原版實時更新。
版本
- 當前更新版本:2021-07-09 版本地址:commit:#133
- 如果您發現任何更新、問題或改進,請隨時 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 嵌入到結構體中。
Bad | Good |
---|---|
|
|
|
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 |
---|---|
|
|
|
|
規范
避免過長的行
避免使用需要讀者水平滾動或過度轉動頭部的代碼行。
我們建議將行長度限制為 99 characters (99個字符).
作者應該在達到這個限制之前換行,
但這不是硬性限制。
允許代碼超過此限制。
一致性
本文中概述的一些標准都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;
但是最重要的是,保持一致.
一致性的代碼更容易維護、是更合理的、需要更少的學習成本、並且隨着新的約定出現或者出現錯誤后更容易遷移、更新、修復 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 |
---|---|
|
|
必須始終提供緩存和記錄器參數,即使用戶希望使用默認值。 |
只有在需要時才提供選項。 |
我們建議實現此模式的方法是使用一個 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。