Uber公司推出的Go語言規(guī)范,建議沒看過的同學(xué)看一遍,里面的規(guī)范很多,不見得每一條都采納,不現(xiàn)實(shí),選一些適合的可以落地執(zhí)行的拿來參考就行。
介紹
本指南的目的是通過詳細(xì)描述在Uber編寫Go代碼的注意事項(xiàng)來管理這種復(fù)雜性。這些規(guī)則的存在是為了保持代碼庫的可管理性,同時還允許工程師有效地使用Go語言的特性。
本指南最初是由Prashant Varanasi和Simon Newton創(chuàng)建的,是為了讓一些同事盡快掌握Go的使用方法。多年來,我們根據(jù)其他人的反饋對它進(jìn)行了修改。
這記錄了我們在 Uber 所遵循的 Go 代碼中的習(xí)慣性約定。其中很多是Go里面的一般準(zhǔn)則,而其他的則是根據(jù)外部資源進(jìn)行擴(kuò)展:
- Effective Go
- Go Common Mistakes
- Go Code Review Comments
我們的目標(biāo)是使代碼樣本準(zhǔn)確地適用于Go的兩個最新的次要版本。
所有代碼在通過golint和go vet運(yùn)行時應(yīng)該是沒有錯誤的。我們建議將您的編輯器設(shè)置為:
- 保存時運(yùn)行 goimports
- 運(yùn)行 golint 和 go vet 檢查錯誤
你可以在這里找到編輯器支持Go工具的信息:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
指南
指向interface的指針
你幾乎不需要一個指向 interface 的指針,interface類型數(shù)據(jù)應(yīng)該直接傳遞,但實(shí)際上 interface 底層是一個指針。
interface 類型包括兩部分:
- 一個指向特定類型的指針。可以將其視為 "類型"。
- 數(shù)據(jù)指針。如果底層數(shù)據(jù)是指針,會被直接存儲。如果底層數(shù)據(jù)是值,那會存儲這個數(shù)據(jù)的指針。
如果你想要接口方法修改基礎(chǔ)數(shù)據(jù),那必須使用指針。
驗(yàn)證接口合法性
在編譯期驗(yàn)證接口的合法性,需要驗(yàn)證的有:
- 驗(yàn)證導(dǎo)出類型在作為API時是否實(shí)現(xiàn)了特定接口
- 實(shí)現(xiàn)一個接口的導(dǎo)出和非導(dǎo)出類型是集合的一部分
- 違反接口合理性無法編譯通過,通知用戶
Bad | Good |
---|---|
|
|
如果*Handler沒有實(shí)現(xiàn)http.Handler接口,那么var _ http.Handler = (*Handler)(nil)語句在編譯期就會報錯;
賦值語句的右邊部門應(yīng)是斷言類型的零值。對于指針類型(像*Handler)、slice和map類型,零值為nil,對于結(jié)構(gòu)體類型,零值為空結(jié)構(gòu)體,下面是空結(jié)構(gòu)體的例子。
type LogHandler struct {
h http.Handler
log *zap.Logger
}
// LogHandler{}是空結(jié)構(gòu)體
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
接收者和接口
使用 值類型 接收者的方法既可以通過值調(diào)用,也可以通過指針調(diào)用。
使用 指針類型 接收者的方法只能通過指針或者 addressable values調(diào)用。
例如:
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"}}
// 值類型可以調(diào)用Read()
sVals[1].Read()
// 值類型調(diào)用Write方法會報編譯錯誤
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// 指針類型 Read 和 Write 方法都可以調(diào)用
sPtrs[1].Read()
sPtrs[1].Write("test")
同樣的,接口可以通過指針調(diào)用,即使這個方法的接收者是指類型。
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
// 這個例子編譯會報錯,因?yàn)閟2Val是值類型,而S2的方法里接收者是指針類型.
// i = s2Val
Effective Go 有一段關(guān)于 Pointers vs. Values 的優(yōu)秀講解
Mutexes的零值是有效的
sync.Mutex 和 sync.RWMutex 的零值是有效的,所以不需要實(shí)例化一個Mutex的指針。
Bad | Good |
---|---|
|
|
如果結(jié)構(gòu)體中包含mutex,在使用結(jié)構(gòu)體的指針時,mutex應(yīng)該是結(jié)構(gòu)體的非指針字段,也不要把mutex內(nèi)嵌到結(jié)構(gòu)體中,即使結(jié)構(gòu)體是非導(dǎo)出類型。
Bad | Good |
---|---|
|
|
隱式嵌入 |
mutex和它 |
在邊界拷貝Slices和Maps
slice 和 map 類型包含指向data數(shù)據(jù)的指針,所以當(dāng)你需要復(fù)制時應(yīng)格外注意。
接收 Slices 和 Maps
如果在函數(shù)調(diào)用中傳遞 map 或 slice, 請記住這個函數(shù)可以修改它。
Bad | Good |
---|---|
|
|
返回 Slices 和 Maps
同樣,請注意用戶對 maps 或 slices 的修改暴露了內(nèi)部狀態(tài)。
Bad | Good |
---|---|
|
|
使用Defer釋放資源
在讀寫文件、使用鎖時,使用 defer 釋放資源
Bad | Good |
---|---|
|
|
調(diào)用 defer 的性能開銷非常小,但如果你需要納秒級別的函數(shù)調(diào)用,那可能需要避免使用 defer。使用 defer 帶來的可讀性 勝過引入其它帶來的性能開銷。defer 尤其適用于適用于那些不僅是內(nèi)存 放在的行數(shù)較多、邏輯較為復(fù)雜的大方法,這些方法中其他代碼邏輯的執(zhí)行成本比 defer 執(zhí)行成本更大。
Channel 大小應(yīng)為 0 或 1
Channels 的大小應(yīng)該是1或無緩沖的。默認(rèn)情況下,channels 是無緩沖的,size為0。其他size需經(jīng)過嚴(yán)格的審查??紤] channel 的size 是如何定義的,是什么造成了 channel 在負(fù)荷情況下被寫滿而無法寫入,以及無法寫入會發(fā)生什么。
Bad | Good |
---|---|
|
|
枚舉類型值從1開始
在Go中聲明枚舉值的標(biāo)準(zhǔn)方法是使用const包iota。由于變量的默認(rèn)值為0,因此枚舉類型的值需要從1開始。
Bad | Good |
---|---|
|
|
當(dāng)你需要將0值視為默認(rèn)行為時,枚舉類型從0開始是有意義的。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
使用time包來處理時間
時間處理很復(fù)雜,關(guān)于時間錯誤預(yù)估有以下這些點(diǎn)。
- 一天有24小時
- 一小時有60分鐘
- 一周有7天
- 一年有365天
- 其他易錯點(diǎn)
舉例來說, 1 表示在一個時間點(diǎn)加上24小時并不一定會產(chǎn)生新的一天。
因此,在處理時間時應(yīng)始終使用"time"包,因?yàn)樗鼤酶踩?zhǔn)確的方式來處理這些不正確的假設(shè)。
用 time.Time 表示瞬時時間
需要瞬時時間語義時,使用time.Time ,在進(jìn)行比較、增加或減少時間段時,使用time.Time包里的方法。
Bad | Good |
---|---|
|
|
用 time.Duration 表示時間段
應(yīng)使用 time.Duration 來表示時間段
Bad | Good |
---|---|
|
|
回到剛剛的例子,在一個瞬時時間加上24小時,怎么加這個 "24小時" 取決于我們的意圖。如果我們想獲取 下一天的當(dāng)前時間,我們應(yīng)該使用 Time.AddDate。如果我們想獲取比當(dāng)前時間晚24小時的瞬時時間, 我們應(yīng)該應(yīng)該使用 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 標(biāo)記: flag 通過支持 time.ParseDuration 來支持 time.Duration
- JSON: encoding/json 通過 [UnmarshalJSON 方法] 支持把 time.Time 解碼為 RFC 3339 字符串
- SQL: database/sql 支持把 DATETIME 或 TIMESTAMP 類型轉(zhuǎn)化為 time.Time
- YAML: gopkg.in/yaml.v2 支持把 time.Time 作為一個 RFC 3339 字符串, 通過支持time.ParseDuration 來支持time.Duration。
如果交互中不支持使用time.Duration,那字段名中應(yīng)包含單位,類型應(yīng)為int或float64。
例如, 由于 encoding/json 不支持 time.Duration 類型, 因此字段名中應(yīng)包含時間單位。
Bad | Good |
---|---|
|
|
如果在交互中不能使用 time.Time,除非有額外約定,否則應(yīng)該使用 string 和 RFC 3339定義的 時間戳格式。默認(rèn)情況下, Time.UnmarshalText 使用這種格式,并可以通過time.RFC3339在Time.Format 和 time.Parse 中使用。
盡管在實(shí)踐中這不是什么問題,但是你需要記住"time"不能解析閏秒時間戳(8728),在計(jì)算中也不考慮閏秒(15190)。因此如果你要比較兩個瞬時時間,比較結(jié)果不會包含這兩個瞬時時間可能會出現(xiàn)的閏秒。
錯誤
錯誤類型
聲明錯誤的選項(xiàng)很少。在為你的代碼選擇合適的用例之前,考慮這些事項(xiàng):
- 調(diào)用方需要匹配錯誤嗎,還是調(diào)用方需要自己處理錯誤。如果需要匹配錯誤,那應(yīng)該聲明頂級 錯誤類型或自定義類型 來讓 errors.Is 或 errors.As 匹配。
- 錯誤信息是靜態(tài)字符串嗎,還是說錯誤信息是需要上下文的動態(tài)字符串。如果是靜態(tài)字符串, 我們可以使用 errors.New,如果是動態(tài)字符串,我們應(yīng)該使用fmt.Errorf來 自定義錯誤類型。
- 我們是否正在傳遞由下游返回的新的錯誤類型,如果是這樣,參考section on error wrapping.
錯誤匹配? | 錯誤信息 | 使用 |
---|---|---|
無 | 靜態(tài) | errors.New
|
無 | 動態(tài) | fmt.Errorf
|
有 | 靜態(tài) | 用errors.New 聲明頂級錯誤類型 |
有 | 動態(tài) | 定制 error 類型 |
舉例,使用 errors.New 表示一個靜態(tài)字符串錯誤。如果調(diào)用方需要匹配并處理這個錯誤, 就把這個錯誤聲明為變量來支持和 errors.Is 匹配。
無錯誤匹配 | 有錯誤匹配 |
---|---|
|
|
對于動態(tài)字符串的錯誤,如果調(diào)用方不需要匹配就是用fmt.Errorf,需要匹配就搞一個自定義error。
無錯誤匹配 | 有錯誤匹配 |
---|---|
|
|
注意,如果你的包里導(dǎo)出了錯誤變量或錯誤類型,那這個錯誤將變成你包里公共API的一部分。
錯誤包裝
調(diào)用函數(shù)失敗時,有三種選擇供你選擇:
- 返回原始錯誤
- 用 fmt.Errorf 和 %w 包裝上下文信息
- 用 fmt.Errorf 和 %v 包裝上下文信息
返回原始錯誤不會附加上下文信息,這樣就保持了原始錯誤類型和信息。比較適用于lib庫類型代碼 展示底層錯誤信息。
如果不是lib庫,就需要增加所需的上下文信息,不然就會出現(xiàn) "connection refused" 這樣非常 模糊的錯誤,理論上應(yīng)該添加上下文,來得到這樣的報錯信息:"call service foo: connection refused"。
在錯誤類型上使用 fmt.Errorf 來添加上下文信息,根據(jù)調(diào)用方不同的使用方式,可以選擇 %w 或 %v 動詞。
- 如果調(diào)用方需要訪問底層錯誤,使用%w動詞,這是一個用來包裝錯誤的動詞,如果你在代碼中使用到了它,請注意 調(diào)用方會對此產(chǎn)生依賴,所以當(dāng)你的包裝的錯誤是用var聲明的已知類型,需要在你的代碼里對其進(jìn)行測試。
- 使用 %v 會混淆你的底層錯誤類型,調(diào)用方將無法進(jìn)行匹配,如果有匹配需求,應(yīng)該使用%w動詞。
當(dāng)為返回錯誤增加上下文信息時,避免在上下文中增加像 "failed to" 這樣的沒啥用的短語,這樣沒用的短語在錯誤 堆棧中堆積起來的話,反而不利于你定位bug。
Bad | Good |
---|---|
|
|
|
|
然而當(dāng)你的錯誤傳給別的系統(tǒng)時,錯誤信息應(yīng)該足夠清晰。(比如, 錯誤信息在日志中以 "Failed" 開頭)
其他參考信息:Don't just check errors, handle them gracefully.
錯誤命名
對于全局變量類型,根據(jù)是否導(dǎo)出使用 Err 或 err 前綴。詳情參考:Prefix Unexported Globals with _.
var (
// 以下兩個錯誤是導(dǎo)出類型,所以他們的命名以 Err 作為開頭,用戶可以使用 errors.Is 來匹配錯誤類型
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// 這個錯誤是非導(dǎo)出類型,不會作為我們公共API的一部分,但是你可以在包內(nèi)使用errors.Is匹配它。
errNotFound = errors.New("not found")
)
對于自定義錯誤類型,請使用Error后綴。
// 這個錯誤是被導(dǎo)出的,用戶可以使用errors.As去匹配它。
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// 這個錯誤是非導(dǎo)出類型,不會作為我們公共API的一部分,但是你可以在包內(nèi)使用errors.As匹配它。
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
todo:errors.Is和errors.As的區(qū)別
處理斷言失敗
在不正確的類型斷言上 使用單返回值來處理會導(dǎo)致 panic, 因此請使用 "comma ok" 習(xí)俗.
Bad | Good |
---|---|
|
|
不要使用Panic
在生產(chǎn)環(huán)境的業(yè)務(wù)代碼避免使用panic。Panics 是級聯(lián)問題cascading failures的主要來源。如果發(fā)生錯誤,函數(shù)必須返回錯誤,讓調(diào)用方?jīng)Q定如何處理這種情況。
Bad | Good |
---|---|
|
|
Panic/recover 不是錯誤處理策略。當(dāng)系統(tǒng)發(fā)生像空指針異常這種 不可恢復(fù)的 Fatal 異常時,才需要使用Panic。唯一的意外情況是項(xiàng)目啟動:如果程序啟動階段出現(xiàn)問題需要拋出異常。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
即使在測試中,優(yōu)先使用t.Fatal 或 t.FailNow 而不是異常,來確保失敗情況被記錄。
Bad | Good |
---|---|
|
|
使用 go.uber.org/atomic
使用 sync/atomic 包的原子操作對數(shù)據(jù)類型進(jìn)行操作(int32, int64, 等.) 。因?yàn)樽约翰僮魅菀淄浭褂迷硬僮鲗ψ兞窟M(jìn)行讀取和修改。
go.uber.org/atomic 通過隱藏基礎(chǔ)類型為這些操作增加了類型安全性,它還包括一個很方便的 atomic.Bool 類型.
Bad | Good |
---|---|
|
|
避免使用全局可變對象
Avoid mutating global variables, instead opting for dependency injection. This applies to function pointers as well as other kinds of values.
Bad | Good |
---|---|
|
|
|
|
避免在公共結(jié)構(gòu)體中內(nèi)嵌類型
嵌入類型會暴露實(shí)現(xiàn)細(xì)節(jié),無法類型演化,讓文檔也變得模糊。
假設(shè)你用AbstractList結(jié)構(gòu)體實(shí)現(xiàn)了公共的 list 方法,避免在其他實(shí)現(xiàn)中內(nèi)嵌AbstractList類型。而是應(yīng)該在其他結(jié)構(gòu)體中顯式聲明list,并在方法實(shí)現(xiàn)中調(diào)用list的方法。
type AbstractList struct {}
// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
// ...
}
Bad | Good |
---|---|
|
|
Go允許內(nèi)嵌類型type embedding作為組合和繼承的折中方案。外部的結(jié)構(gòu)體會獲得內(nèi)嵌類型的隱式拷貝。默認(rèn)情況下,內(nèi)嵌類型的方法會嵌入實(shí)例的同一方法。
外部的結(jié)構(gòu)體會獲取嵌入類型的同名字段。如果嵌入類型的字段是公開(public)的,那嵌入后也是公開的。為保證向后兼容性,外部結(jié)構(gòu)體未來每個版本都需要保留嵌入類型。
很少場景需要嵌入類型,雖然嵌入類型很方便,讓你避免編寫冗長的方法。
即使是用interface而不是結(jié)構(gòu)體來嵌入方法,這是給開發(fā)人員帶來了一定的靈活性,但是仍然暴露了具體實(shí)現(xiàn)列表的抽象細(xì)節(jié)。
Bad | Good |
---|---|
|
|
不管是嵌入結(jié)構(gòu)體還是嵌入接口,都會限制類型的演化。
- 若嵌入接口,當(dāng)你增加一個方法是一種破壞性改變
- 若嵌入結(jié)構(gòu)體,當(dāng)你刪除一個方法是一種破壞性改變
- 刪除嵌入類型是一種破壞性改變
- 即使用滿足接口約束的類型去替換嵌入類型,也是一種破壞性改變
盡管編寫內(nèi)嵌類型已實(shí)現(xiàn)的方法是乏味的。但是這些工作隱藏了實(shí)現(xiàn)細(xì)節(jié),留下了更多更改的機(jī)會, 并消除了在文檔中發(fā)現(xiàn)完整List接口的間接方法。
避免使用內(nèi)建命名
Go語言的spec中列舉了一些內(nèi)建命名,在你的Go程序中應(yīng)該避免使用預(yù)聲明的標(biāo)識符;
根據(jù)上下文的不同,用預(yù)聲明標(biāo)識符命名變量可能會在當(dāng)前作用域下覆蓋官方標(biāo)識符,讓你的代碼變得難以理解。最好的情況下,編譯器會直接報錯,最糟糕的情況下,這樣的代碼會引入難以排查的bug。
Bad | Good |
---|---|
|
|
|
|
注意當(dāng)你使用預(yù)聲明標(biāo)識符時編譯器不會報錯,但是像 go vet 這樣的工具會告訴你標(biāo)識符被覆蓋的情況。
避免使用init()
盡可能避免使用init()。如果實(shí)在依賴 init(),可以使用以下方式:
- 不管程序環(huán)境或調(diào)用方式如何,初始化要完全確定。
- 避免依賴其他init()函數(shù)的順序或者產(chǎn)生的結(jié)果。雖然init()順序是明確的,但是代碼可以更改。init()函數(shù)之間的關(guān)系會讓代碼變得易錯和脆弱。
- 避免讀寫全局變量、環(huán)境變量,比如機(jī)器信息、環(huán)境變量、工作目錄,程序的參數(shù)和輸入等等。
- 避免 I/O 操作,比如文件系統(tǒng),網(wǎng)絡(luò)和系統(tǒng)調(diào)用。
如果代碼不能滿足這些需求,那可能屬于幫助代碼,需要作為main()函數(shù)的一部分進(jìn)行調(diào)用(或者封裝初始化邏輯,讓main 函數(shù)去調(diào)用)。需要注意的是,被其他模塊依賴的代碼應(yīng)該完全指定初始化順序的確定性,而不是依賴"初始化魔法"。
Bad | Good |
---|---|
|
|
|
|
但是在某些情況下,init()函數(shù)可能更具優(yōu)勢:
- 單個賦值語句中無法表示的復(fù)雜表達(dá)式
- 插件鉤子,比如 database/sql,編碼信息注冊表等
- 對 Google Cloud Functions 和其他形式確定性預(yù)計(jì)算的優(yōu)化
優(yōu)雅退出主函數(shù)
Go程序使用os.Exit 或 log.Fatal*來立即退出。(Panic 不是優(yōu)雅的程序退出方式,可以參考 don't panic)
應(yīng)該只在main()函數(shù)里調(diào)用os.Exit 或 log.Fatal*函數(shù)。其他函數(shù)應(yīng)該返回錯誤來表示失敗,在main中進(jìn)行退出。
Bad | Good |
---|---|
|
|
程序中多個函數(shù)都能退出的話會有一些問題:
- 不明顯的控制流:多個函數(shù)都能退出的話,找出程序的控制流會變得困難。
- 測試?yán)щy:如果一個函數(shù)讓程序退出,那它也會讓測試退出。這樣會讓函數(shù)難以測試。而且可能會讓go text無法測試其他函數(shù)。
- 跳過清理:當(dāng)一個函數(shù)退出程序時,會跳過已經(jīng)進(jìn)入defer隊(duì)列的函數(shù)調(diào)用。這樣會增加跳過清理任務(wù)的風(fēng)險。
一次性退出
有條件的情況下,main()函數(shù)中最好只調(diào)用os.Exit 或 log.Fatal 一次。如果有多種錯誤情況會停止 程序的執(zhí)行,將這些錯誤放在一個獨(dú)立的函數(shù)中,并返回錯誤,main()中處理錯誤并退出。
把所有的關(guān)鍵邏輯放在一個獨(dú)立的可測試的函數(shù)中,會讓你的main()函數(shù)變得簡短。
Bad | Good |
---|---|
|
|
在序列化結(jié)構(gòu)體中使用字段標(biāo)簽。
要編碼成JSON、YAML或其他支持tag格式的結(jié)構(gòu)體字段應(yīng)該用指定對應(yīng)項(xiàng)tag標(biāo)簽進(jìn)行注釋。
Bad | Good |
---|---|
|
|
Rationale: 結(jié)構(gòu)體的序列化方式是不同系統(tǒng)通信的契約。修改結(jié)構(gòu)體的結(jié)構(gòu)和字段會破壞這個契約。在結(jié)構(gòu)體中聲明tag 可以防止重構(gòu)結(jié)構(gòu)體中意外違反約定。
性能
性能方面的指導(dǎo)準(zhǔn)則只適用于高頻調(diào)用場景。
使用 strconv 而不是 fmt
當(dāng)需要原始類型和字符串互相轉(zhuǎn)化時,strconv
比fmt
性能更好。
Bad | Good |
---|---|
|
|
|
|
避免 字符串到字節(jié)的轉(zhuǎn)化
不要在for循環(huán)中創(chuàng)建[]byte類型,應(yīng)該在for循環(huán)開始前把[]byte數(shù)據(jù)準(zhǔn)備好。
Bad | Good |
---|---|
|
|
|
|
預(yù)先指定容器類型的容量
盡可能指定容器類型變量的容量來預(yù)先分配容器類型所需的內(nèi)存大小。這樣可以預(yù)防由于后續(xù)由分配元素 (由于拷貝或重新指定容器大?。┒鴮?dǎo)致的內(nèi)存分配。
指定Map容量
如果有可能,用make來初始化map類型,并指定map的大小。
make(map[T1]T2, hint)
使用 make()
初始化map時,提供一個容量來執(zhí)行size,這樣會減少后續(xù)將給map添加元素時引起的內(nèi)存分配。
注意,和 slice 不同,給map指定容量不意味著搶占式內(nèi)存分配完成,而是會用于預(yù)估的哈希表內(nèi)部 buckets。因此,當(dāng)你給 map 添加元素,或者給 map 指定值時,仍有可能發(fā)生內(nèi)存分配。
Bad | Good |
---|---|
|
|
|
|
指定Slice容量
如果有可能的話,在使用make()
初始化slice的時候提供容量大小,尤其是后面需要 append 操作時。
make([]T, length, capacity)
和 map 不同,slice的容量不是一個提示:編譯器會根據(jù) make()
提供的容量信息申請足夠的內(nèi)存, 這意味著后續(xù)的 append()
操作不會申請內(nèi)存(除非slice的長度和容量相等,這樣的話后續(xù)添加元素 會申請內(nèi)存來調(diào)整 slice 的大?。?。
Bad | Good |
---|---|
|
|
|
|
規(guī)范
避免代碼過長
避免由于長度過長而需要水平滾動或者太需要轉(zhuǎn)動頭部的代碼。
我們建議一行代碼長度為 99個字符,如果代碼超過了這個限制就應(yīng)該換行。但是這也不是絕對的,代碼 可以超過這個限制。
一致性
本文的一些指導(dǎo)準(zhǔn)則可以被客觀評估,其他準(zhǔn)則可以根據(jù)實(shí)際情況進(jìn)行選擇。
但是最重要是,在你的代碼中要保持一致。
一致性的代碼更易于維護(hù),更容易合理化,需要的認(rèn)知開銷較少;當(dāng)新的管理出現(xiàn)時或 bug 被修復(fù)后也更易于 遷移和更新。
與之相反,如果單個代碼庫中有多種沖突的風(fēng)格,會讓維護(hù)成本升高、不確定性增高、認(rèn)知不協(xié)調(diào),這些問題會 導(dǎo)致開發(fā)效率降低,code review 困難,且容易產(chǎn)生 bug。
當(dāng)你在代碼庫中實(shí)施標(biāo)準(zhǔn)時,建議最低在包層面進(jìn)行修改:在子包層面進(jìn)行應(yīng)用違反了上述約定,因?yàn)樵谝环N代碼 里面引入了多種風(fēng)格。
相似聲明放一組
Go語言支持組引用。
Bad | Good |
---|---|
|
|
組聲明同樣適用于常量、變量和類型聲明。
Bad | Good |
---|---|
|
|
注意只把相關(guān)的變量聲明到一個組里,不想管的聲明放在多個組里。
Bad | Good |
---|---|
|
|
組聲明不限制在哪使用。比如,你可以在函數(shù)中使用組聲明。
Bad | Good |
---|---|
|
|
例外:對于變量聲明,尤其是函數(shù)中的變量聲明,不管他們之間是否有關(guān)系,都應(yīng)該被放在一個組內(nèi)。
Bad | Good |
---|---|
|
|
包導(dǎo)入順序
包中應(yīng)該有兩種導(dǎo)入順序:
-
標(biāo)準(zhǔn)庫 -
其他庫
默認(rèn)情況下,應(yīng)該使用 goimports 的導(dǎo)入順序。
Bad | Good |
---|---|
|
|
包命名
當(dāng)命名包時,應(yīng)按照下面原則命名:
-
全小寫字母。無大寫字母或下劃線。 -
大多數(shù)導(dǎo)入包的情況下,不需要對包重新命名。 -
簡短而簡潔,因?yàn)楫?dāng)你使用包名時你都需要完成輸入包名稱。 -
不要使用復(fù)數(shù)。比如:命名為 net/url
, 而不是net/urls
。 -
不要使用"common", "util", "shared", 或 "lib"。這些包含有信息太少了。
可以參考 Package Names 和 Style guideline for Go packages.
函數(shù)命名
我們遵守 Go 社區(qū) MixedCaps for function names 約定。一種其他情況是使用測試函數(shù)。測試函數(shù) 命名可以包含下劃線以便于相關(guān)測試函數(shù)進(jìn)行分組。比如:TestMyFunction_WhatIsBeingTested
。
導(dǎo)入別名
如果包名稱和導(dǎo)入路徑最后一個元素不匹配,就需要使用導(dǎo)入別名。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
其他情況下,除非幾個包之間有導(dǎo)入沖突,否則應(yīng)該避免使用導(dǎo)入別名。
Bad | Good |
---|---|
|
|
函數(shù)分組和排序
-
函數(shù)應(yīng)該按照大概調(diào)用順序排序。 -
一個文件中的函數(shù)應(yīng)該按照接收者分組。
因此,導(dǎo)入的函數(shù)時,應(yīng)該放在 struct
, const
, var
的下面。
像 newXYZ()
/NewXYZ()
這樣的函數(shù)可能會出現(xiàn)在類型定義下、接收者的其他方法之上。
由于函數(shù)是按照接收者進(jìn)行分組的,普通的工具函數(shù)應(yīng)該放在文件末尾。
Bad | Good |
---|---|
|
|
減少嵌套
代碼應(yīng)通過盡早處理錯誤/特殊情況盡早處理/循環(huán)中使用 continue 等手段,來減少嵌套代碼過多問題。
Bad | Good |
---|---|
|
|
沒用的else
如啊a變量在兩個if分支中都進(jìn)行賦值操作,則可以被替換為只在一個if分支中聲明。
Bad | Good |
---|---|
|
|
頂層變量聲明
在頂層使用var
來聲明變量。不要指定類型,除非它和表達(dá)式的類型不同。
Bad | Good |
---|---|
|
|
如果表達(dá)式的類型和所需類型不一樣,需要指定類型。
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F returns an object of type myError but we want error.
非導(dǎo)出變量使用_前綴
對于非導(dǎo)出類型的變量,在用var
s and const
聲明時加上_
前綴,來表示他們是全局符號。
原因:頂層聲明的變量作用域一般是包范圍。用一個常見的名字可能會導(dǎo)致在其他包中被意外修改。
Bad | Good |
---|---|
|
|
異常:非導(dǎo)出的錯誤類型一般使用不帶 _
的 err
前綴。參考Error Naming.
結(jié)構(gòu)體內(nèi)嵌類型
嵌入類型應(yīng)該放在結(jié)構(gòu)體的最上面,應(yīng)該和結(jié)構(gòu)體的常規(guī)字段用一個空行分隔開。
Bad | Good |
---|---|
|
|
內(nèi)嵌類型會帶來足夠的好處,比如在語義上會增加或增強(qiáng)功能。但應(yīng)該在對用戶沒有影響的情況下使用內(nèi)嵌。(參考: 避免在公共結(jié)構(gòu)中嵌入類型).
例外:即使是未導(dǎo)出類型,Mutex 也不應(yīng)該被內(nèi)嵌。參考:: Mutex的零值是有效的.
這些情況下避免內(nèi)嵌:
-
單純?yōu)榱吮憷兔烙^。 -
讓外部類型構(gòu)造起來或使用起來更困難。 -
影響了外部的零值。如果外部類型的零值是有用的,嵌入類型應(yīng)該也有一個有用的零值。 -
作為嵌入類型的副作用,公開外部類型的不相關(guān)函數(shù)或字段。 -
公開非導(dǎo)出類型。 -
影響外部類型的復(fù)制語義。 -
影響外部類型的API或類型語義。 -
影響內(nèi)部類型的非規(guī)范形式。 -
公開外部類型的詳細(xì)實(shí)現(xiàn)信息。 -
允許用戶觀察和控制內(nèi)部類型。 -
通過包裝的形式改變了內(nèi)部函數(shù)的行為,這種包裝的方式會給用戶造成意外觀感。
簡單概括,使用嵌入類型時要明確目的。一個不錯的方式是:"這些嵌入的字段/方法是否需要被直接添加到外部 類型",如果答案是"一些"或者"No",不要使用內(nèi)嵌類型,而是使用命名字段。
Bad | Good |
---|---|
|
|
|
|
|
|
本地變量聲明
如果將變量聲明為某個值,應(yīng)該使用短變量命名方式::=
。
Bad | Good |
---|---|
|
|
然而,有些情況下用 var
會讓聲明語句更加清晰,比如聲明空slice.
Bad | Good |
---|---|
|
|
nil值的slice有效
nil
代表長度為0的有效slice, 意味著:
-
你不應(yīng)該聲明一個長度為0的空slice,而是用
nil
來代替。Bad Good if x == "" { return []int{} }
if x == "" { return nil }
-
要檢查slice是否為空,不應(yīng)該檢查
nil
, 而是用長度判斷len(s) == 0
。Bad Good func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
-
用
var
聲明的零值slice是有效的,沒必要用make
來創(chuàng)建。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 的slice有效,但是它不等于長度為0的 slice。在一些情況下(比如說序列化), 這兩種slice的表現(xiàn)不同。
縮小變量作用域
盡可能減小變量的作用域。如果與 減少嵌套 沖突,就不要縮小。
Bad | Good |
---|---|
|
|
但是如果作用域是 if 范圍之外,不應(yīng)該減少作用域。
Bad | Good |
---|---|
|
|
不面參數(shù)語義不明確
在函數(shù)中裸傳參數(shù)值會讓代碼語義不明確,可以添加 C 風(fēng)格(/* ... */
)的注釋。
Bad | Good |
---|---|
|
|
當(dāng)然,更好的處理方式將上面的 bool
換成自定義類型。因?yàn)槲磥砜赡懿粌H僅局限于兩個bool值(true/false)。
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status = iota + 1
StatusDone
// 獲取未來會有 StatusInProgress 枚舉
)
func printInfo(name string, region Region, status Status)
字符串中避免轉(zhuǎn)義
Go中支持 字符串原始值,當(dāng)需要 轉(zhuǎn)義時,盡量使用 "`" 來包裝字符串。
Bad | Good |
---|---|
|
|
初始化結(jié)構(gòu)體
初始化結(jié)構(gòu)體時聲明字段名
在你初始化結(jié)構(gòu)時,幾乎應(yīng)該始終指定字段名。目前由go vet
強(qiáng)制執(zhí)行。
Bad | Good |
---|---|
|
|
例外:當(dāng)有 3 個或更少的字段時,測試表中的字段名也許可以省略。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
省略結(jié)構(gòu)體中的零值字段
當(dāng)初始化結(jié)構(gòu)體字段時,除非需要提供一個有意義的上下文,否則需要忽略對零值字段進(jìn)行賦值。因?yàn)镚o 會自動給這些零值字段進(jìn)行填充。
Bad | Good |
---|---|
|
|
這種行為讓我們忽略了上下文無關(guān)的噪音信息。只關(guān)注有意義的特殊值。
當(dāng)零值代表有意義的上下文時需要提供零值。比如在 表驅(qū)動測試 中零值字段 是有意義的。
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
空結(jié)構(gòu)體用var聲明
當(dāng)結(jié)構(gòu)體中所有的字段都為空時,用 var
來聲明結(jié)構(gòu)體。
Bad | Good |
---|---|
|
|
這種 零值結(jié)構(gòu)體 和具有非零值字段的結(jié)構(gòu)體有所不同,和 map初始化 更相似, 和我們更想用的 [聲明空Slices][聲明空Slices] 更匹配。
初始化結(jié)構(gòu)體引用
初始化結(jié)構(gòu)引用時,請使用&T{}
代替new(T)
,以使其與結(jié)構(gòu)體初始化一致。
Bad | Good |
---|---|
|
|
初始化Map
優(yōu)先使用make來創(chuàng)建空map,這樣使得map的初始化不同于聲明,而且你還可以在 make 中添加map的大小提示。
Bad | Good |
---|---|
|
|
聲明和初始化在形式上相似 |
聲明和初始化在形式上隔離 |
盡可能在 make()
中制定map的初始化容量,可以參考:Specifying Map Capacity Hints。
另外,如果map初始化的時候需要賦值固定信息,使用 map literals 方式來初始化。
Bad | Good |
---|---|
|
|
原則上:在初始化map時增加一組固定的元素,就使用map literals。否則就使用 make
(如果可以, 盡可能指定map的容量)。
在Printf外面格式化字符串
如果你在函數(shù)外聲明 Printf
風(fēng)格 函數(shù)的格式字符串,請將其設(shè)置為 const
常量。
這有助于go vet對格式字符串執(zhí)行靜態(tài)分析。
Bad | Good |
---|---|
|
|
命名Printf樣式函數(shù)
使用Printf
函數(shù)時,應(yīng)保證go vet
可以檢測到他的格式化字符串。
這意味著你需要使用預(yù)定義的Printf
函數(shù)名稱,go vet
會默認(rèn)檢查這些。更多信息,請參考:Printf family
如果不能使用預(yù)定義的名稱,請以 f 結(jié)束選擇的名稱:Wrapf
,而不是Wrap
。go vet
可以要求檢查特定的 Printf
樣式名稱,但名稱必須以f
結(jié)尾。
$ go vet -printfuncs=wrapf,statusf
參考 go vet: Printf family check.
Patterns
Test Tables
當(dāng)你的測試用例形式上重復(fù)時,用 subtests 方式編寫case會讓測試用例看起來更加簡潔。
Bad | Good |
---|---|
|
|
顯然,如果你用了test table的方式,在拓展測試用例時也會顯得更加清晰。
我們遵守這樣的準(zhǔn)則:搞一個slice類型的struct測試用例,每個測試case叫做tt
。然后使用give
和want
說明測試用例的輸入和輸出。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
對于并行測試,比如一些特殊的循環(huán) (比如那些生產(chǎn) goroutine 或 在循環(huán)中捕獲引用的循環(huán)), 需要 注意在循環(huán)中明確分配循環(huán)變量來確保不會產(chǎn)生閉包。
tests := []struct{
give string
// ...
}{
// ...
}
for _, tt := range tests {
tt := tt // for t.Parallel
t.Run(tt.give, func(t *testing.T) {
t.Parallel()
// ...
})
}
在上面的例子中,由于循環(huán)中使用了 t.Parallel()
,我們必須在外部循環(huán)中聲明一個 tt
變量。如果不這么做,大多數(shù)測試用例都會收到一個非預(yù)期的 tt
,或是一個在運(yùn)行期改變的值。
函數(shù)功能選項(xiàng)API
功能選項(xiàng)是一種模式,你可以聲明一個對用戶不透明的 Option
類型,在一些內(nèi)部結(jié)構(gòu)中記錄信息。函數(shù)接收不定長的參數(shù)選項(xiàng),并根據(jù)參數(shù)做不同的行為。
對于需要拓展參數(shù)的構(gòu)造方法或是其他需要可選參數(shù)的公共API可以考慮這種模式,對于參數(shù)在三個及以上 的函數(shù)更應(yīng)該考慮。
Bad | Good |
---|---|
|
|
即使用戶默認(rèn)不需要 cache 和 logger,也需要提供這倆參數(shù)。
|
Options 只在需要時才被提供。
|
我們建議這種模式的實(shí)現(xiàn)方式是 提供一個 Option
接口,里面有一個非導(dǎo)出類型方法,在一個非 導(dǎo)出類型的 options
結(jié)構(gòu)體中記錄選項(xiàng)。
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 創(chuàng)建一個連接
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
還有一種用閉包實(shí)現(xiàn)這種方法的模式,但我們認(rèn)為上面提供的這種模式給作者提供了更高的靈活性,更 易于調(diào)試和測試。這種方式可以在測試和mock中進(jìn)行比較,而閉包方式難以做到。此外,它允許 option 實(shí)現(xiàn)其他接口,比如 fmt.Stringer
,會 string 類型的可讀性更高。
還可以參考:
-
Self-referential functions and the design of options
-
Functional options for friendly APIs
Linting
比其他任何 "神圣" linter 工具更重要的是,在你的代碼庫里使用一致性的 lint 工具。
我們建議最少要使用下面這些 linters 工具嗎,因?yàn)槲覀冋J(rèn)為這些工具可以幫你捕獲最常見的問題,有助于 在沒有規(guī)定的前提下提高代碼質(zhì)量:
-
errcheck 確保錯誤被處理
-
goimports 格式化代碼和管理包引用
-
golint 指出常見的文本錯誤
-
govet 分析代碼中的常見錯誤
-
staticcheck 各種靜態(tài)分析檢查
Lint Runners
由于優(yōu)秀的性能表現(xiàn),我們推薦 golangci-lint 作為Go代碼的首選 lint 工具。這個倉庫有在一個.golangci.yml 例子,里面有配置的 linters 工具和設(shè)置。
golangci-lint 有一系列 various linters 可供使用。建議將這些 linters 作為基礎(chǔ)集合, 我們鼓勵團(tuán)隊(duì)內(nèi)部將其他有意義的 linters 工具在他們的項(xiàng)目中進(jìn)行使用。