Uber Go 語言編碼規範
Uber Go 語言編碼規範
是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 項目,諸如被 Gopher 圈熟知的 、 等。2018 年年末 Uber 將內部的 開源到 GitHub,經過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注。本文是該規範的中文版本。本版本會根據原版實時更新。
## 版本
- 當前更新版本:2019-11-13 版本地址:
- 如果您發現任何更新、問題或改進,請隨時 fork 和 PR
- Please feel free to fork and PR if you find any updates, issues or improvement.
目錄
介紹
樣式 (style) 是支配我們代碼的慣例。術語樣式
有點用詞不當,因為這些約定涵蓋的範圍不限於由 gofmt 替我們處理的源文件格式。
本指南的目的是通過詳細描述在 Uber 編寫 Go 代碼的注意事項來管理這種複雜性。這些規則的存在是為了使代碼庫易於管理,同時仍然允許工程師更有效地使用 Go 語言功能。
該指南最初由 和 編寫,目的是使一些同事能快速使用 Go。多年來,該指南已根據其他人的反饋進行了修改。
本文檔記錄了我們在 Uber 遵循的 Go 代碼中的慣用約定。其中許多是 Go 的通用準則,而其他擴展準則依賴於下面外部的指南:
所有代碼都應該通過golint
和go vet
的檢查並無錯誤。我們建議您將編輯器設置為:
- 保存時運行
goimports
- 運行
golint
和go vet
檢查錯誤
您可以在以下 Go 編輯器工具支持頁面中找到更為詳細的信息:
指導原則
指向 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
中有一段關於 的精彩講解。
零值 Mutex 是有效的
零值 sync.Mutex
和 sync.RWMutex
是有效的。所以指向 mutex 的指針基本是不必要的。
Bad | Good |
---|---|
“`go mu := new(sync.Mutex) mu.Lock() “` | “`go var mu sync.Mutex mu.Lock() “` |
如果你使用結構體指針,mutex 可以非指針形式作為結構體的組成字段,或者更好的方式是直接嵌入到結構體中。
如果是私有結構體類型或是要實現 Mutex 接口的類型,我們可以使用嵌入 mutex 的方法:
“`go type smap struct { sync.Mutex // only for unexported types(僅適用於非導出類型) data map[string]string } func newSMap() *smap { return &smap{ data: make(map[string]string), } } func (m *smap) Get(k string) string { m.Lock() defer m.Unlock() return m.data[k] } “` | “`go type SMap struct { mu sync.Mutex // 對於導出類型,請使用私有鎖 data map[string]string } func NewSMap() *SMap { return &SMap{ data: make(map[string]string), } } func (m *SMap) Get(k string) string { m.mu.Lock() defer m.mu.Unlock() return m.data[k] } “` |
為私有類型或需要實現互斥接口的類型嵌入。 | 對於導出的類型,請使用專用字段。 |
在邊界處拷貝 Slices 和 Maps
slices 和 maps 包含了指向底層數據的指針,因此在需要複製它們時要特別注意。
接收 Slices 和 Maps
請記住,當 map 或 slice 作為函數參數傳入時,如果您存儲了對它們的引用,則用戶可以對其進行修改。
Bad | Good |
---|---|
“`go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := … d1.SetTrips(trips) // 你是要修改 d1.trips 嗎? trips[0] = … “` | “`go func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := … d1.SetTrips(trips) // 這裏我們修改 trips[0],但不會影響到 d1.trips trips[0] = … “` |
返回 slices 或 maps
同樣,請注意用戶對暴露內部狀態的 map 或 slice 的修改。
Bad | Good |
---|---|
“`go type Stats struct { mu sync.Mutex counters map[string]int } // Snapshot 返回當前狀態。 func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() return s.counters } // snapshot 不再受互斥鎖保護 // 因此對 snapshot 的任何訪問都將受到數據競爭的影響 // 影響 stats.counters snapshot := stats.Snapshot() “` | “`go type Stats struct { mu sync.Mutex counters map[string]int } func (s *Stats) Snapshot() map[string]int { s.mu.Lock() defer s.mu.Unlock() result := make(map[string]int, len(s.counters)) for k, v := range s.counters { result[k] = v } return result } // snapshot 現在是一個拷貝 snapshot := stats.Snapshot() “` |
使用 defer 釋放資源
使用 defer 釋放資源,諸如文件和鎖。
Bad | Good |
---|---|
“`go p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // 當有多個 return 分支時,很容易遺忘 unlock “` | “`go p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // 更可讀 “` |
Defer 的開銷非常小,只有在您可以證明函數執行時間處於納秒級的程度時,才應避免這樣做。使用 defer 提升可讀性是值得的,因為使用它們的成本微不足道。尤其適用於那些不僅僅是簡單內存訪問的較大的方法,在這些方法中其他計算的資源消耗遠超過 defer
。
Channel 的 size 要麼是 1,要麼是無緩衝的
channel 通常 size 應為 1 或是無緩衝的。默認情況下,channel 是無緩衝的,其 size 為零。任何其他尺寸都必須經過嚴格的審查。考慮如何確定大小,是什麼阻止了 channel 在負載下被填滿並阻止寫入,以及發生這種情況時發生了什麼。
Bad | Good |
---|---|
“`go // 應該足以滿足任何情況! c := make(chan int, 64) “` | “`go // 大小:1 c := make(chan int, 1) // 或者 // 無緩衝 channel,大小為 0 c := make(chan int) “` |
枚舉從 1 開始
在 Go 中引入枚舉的標準方法是聲明一個自定義類型和一個使用了 iota 的 const 組。由於變量的默認值為 0,因此通常應以非零值開頭枚舉。
Bad | Good |
---|---|
“`go type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 “` | “`go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 “` |
在某些情況下,使用零值是有意義的(枚舉從零開始),例如,當零值是理想的默認行為時。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
錯誤類型
Go 中有多種聲明錯誤(Error) 的選項:
- 對於簡單靜態字符串的錯誤
- 用於格式化的錯誤字符串
- 實現
Error()
方法的自定義類型 - 用 的 Wrapped errors
返回錯誤時,請考慮以下因素以確定最佳選擇:
- 這是一個不需要額外信息的簡單錯誤嗎?如果是這樣, 足夠了。
- 客戶需要檢測並處理此錯誤嗎?如果是這樣,則應使用自定義類型並實現該
Error()
方法。 - 您是否正在傳播下游函數返回的錯誤?如果是這樣,請查看本文後面有關錯誤包裝 部分的內容。
- 否則 就可以了。
如果客戶端需要檢測錯誤,並且您已使用創建了一個簡單的錯誤 ,請使用一個錯誤變量。
Bad | Good |
---|---|
“`go // package foo func Open() error { return errors.New(“could not open”) } // package bar func use() { if err := foo.Open(); err != nil { if err.Error() == “could not open” { // handle } else { panic(“unknown error”) } } } “` | “`go // package foo var ErrCouldNotOpen = errors.New(“could not open”) func Open() error { return ErrCouldNotOpen } // package bar if err := foo.Open(); err != nil { if err == foo.ErrCouldNotOpen { // handle } else { panic(“unknown error”) } } “` |
如果您有可能需要客戶端檢測的錯誤,並且想向其中添加更多信息(例如,它不是靜態字符串),則應使用自定義類型。
Bad | Good |
---|---|
“`go func open(file string) error { return fmt.Errorf(“file %q not found”, file) } func use() { if err := open(); err != nil { if strings.Contains(err.Error(), “not found”) { // handle } else { panic(“unknown error”) } } } “` | “`go type errNotFound struct { file string } func (e errNotFound) Error() string { return fmt.Sprintf(“file %q not found”, e.file) } func open(file string) error { return errNotFound{file: file} } func use() { if err := open(); err != nil { if _, ok := err.(errNotFound); ok { // handle } else { panic(“unknown error”) } } } “` |
直接導出自定義錯誤類型時要小心,因為它們已成為程序包公共 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)
一個(函數/方法)調用失敗時,有三種主要的錯誤傳播方式:
- 如果沒有要添加的其他上下文,並且您想要維護原始錯誤類型,則返回原始錯誤。
-
添加上下文,使用 以便錯誤消息提供更多上下文 , 可用於提取原始錯誤。
Use fmt.Errorf if the callers do not need to detect or handle that specific error case. -
如果調用者不需要檢測或處理的特定錯誤情況,使用 。
建議在可能的地方添加上下文,以使您獲得諸如“調用服務 foo:連接被拒絕”之類的更有用的錯誤,而不是諸如“連接被拒絕”之類的模糊錯誤。
在將上下文添加到返回的錯誤時,請避免使用“failed to”之類的短語來保持上下文簡潔,這些短語會陳述明顯的內容,並隨着錯誤在堆棧中的滲透而逐漸堆積:
Bad | Good |
---|---|
“`go s, err := store.New() if err != nil { return fmt.Errorf( “failed to create new store: %s”, err) } “` | “`go s, err := store.New() if err != nil { return fmt.Errorf( “new store: %s”, err) } “` |
“` failed to x: failed to y: failed to create new store: the error “` | “` x: y: new store: the error “` |
但是,一旦將錯誤發送到另一個系統,就應該明確消息是錯誤消息(例如使用err
標記,或在日誌中以”Failed”為前綴)。
另請參見 . 不要只是檢查錯誤,要優雅地處理錯誤
處理類型斷言失敗
的單個返回值形式針對不正確的類型將產生 panic。因此,請始終使用“comma ok”的慣用法。
Bad | Good |
---|---|
“`go t := i.(string) “` | “`go t, ok := i.(string) if !ok { // 優雅地處理錯誤 } “` |
不要 panic
在生產環境中運行的代碼必須避免出現 panic。panic 是 級聯失敗的主要根源 。如果發生錯誤,該函數必須返回錯誤,並允許調用方決定如何處理它。
Bad | Good |
---|---|
“`go func foo(bar string) { if len(bar) == 0 { panic(“bar must not be empty”) } // … } func main() { if len(os.Args) != 2 { fmt.Println(“USAGE: foo “) os.Exit(1) } foo(os.Args[1]) } “` | “`go func foo(bar string) error { if len(bar) == 0 { return errors.New(“bar must not be empty”) } // … return nil } func main() { if len(os.Args) != 2 { fmt.Println(“USAGE: foo “) os.Exit(1) } if err := foo(os.Args[1]); err != nil { panic(err) } } “` |
panic/recover 不是錯誤處理策略。僅當發生不可恢復的事情(例如:nil 引用)時,程序才必須 panic。程序初始化是一個例外:程序啟動時應使程序中止的不良情況可能會引起 panic。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
即使在測試代碼中,也優先使用t.Fatal
或者t.FailNow
而不是 panic 來確保失敗被標記。
Bad | Good |
---|---|
“`go // func TestFoo(t *testing.T) f, err := ioutil.TempFile(“”, “test”) if err != nil { panic(“failed to set up test”) } “` | “`go // func TestFoo(t *testing.T) f, err := ioutil.TempFile(“”, “test”) if err != nil { t.Fatal(“failed to set up test”) } “` |
使用 go.uber.org/atomic
使用 包的原子操作對原始類型 (int32
, int64
等)進行操作,因為很容易忘記使用原子操作來讀取或修改變量。
通過隱藏基礎類型為這些操作增加了類型安全性。此外,它包括一個方便的atomic.Bool
類型。
Bad | Good |
---|---|
“`go type foo struct { running int32 // atomic } func (f* foo) start() { if atomic.SwapInt32(&f.running, 1) == 1 { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running == 1 // race! } “` | “`go type foo struct { running atomic.Bool } func (f *foo) start() { if f.running.Swap(true) { // already running… return } // start the Foo } func (f *foo) isRunning() bool { return f.running.Load() } “` |
性能
性能方面的特定準則只適用於高頻場景。
優先使用 strconv 而不是 fmt
將原語轉換為字符串或從字符串轉換時,strconv
速度比fmt
快。
Bad | Good |
---|---|
“`go for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } “` | “`go for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } “` |
“` BenchmarkFmtSprint-4 143 ns/op 2 allocs/op “` | “` BenchmarkStrconv-4 64.2 ns/op 1 allocs/op “` |
避免字符串到字節的轉換
不要反覆從固定字符串創建字節 slice。相反,請執行一次轉換並捕獲結果。
Bad | Good |
---|---|
“`go for i := 0; i < b.N; i++ { w.Write([]byte(“Hello world”)) } “` | “`go data := []byte(“Hello world”) for i := 0; i < b.N; i++ { w.Write(data) } “` |
“` BenchmarkBad-4 50000000 22.2 ns/op “` | “` BenchmarkGood-4 500000000 3.25 ns/op “` |
盡量初始化時指定 Map 容量
在盡可能的情況下,在使用 make()
初始化的時候提供容量信息
make(map[T1]T2, hint)
為 make()
提供容量信息(hint)嘗試在初始化時調整 map 大小,
這減少了在將元素添加到 map 時增長和分配的開銷。
注意,map 不能保證分配 hint 個容量。因此,即使提供了容量,添加元素仍然可以進行分配。
Bad | Good |
---|---|
“`go m := make(map[string]os.FileInfo) files, _ := ioutil.ReadDir(“./files”) for _, f := range files { m[f.Name()] = f } “` | “`go files, _ := ioutil.ReadDir(“./files”) m := make(map[string]os.FileInfo, len(files)) for _, f := range files { m[f.Name()] = f } “` |
`m` 是在沒有大小提示的情況下創建的; 在運行時可能會有更多分配。 | `m` 是有大小提示創建的;在運行時可能會有更少的分配。 |
規範
一致性
本文中概述的一些標準都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;
但是最重要的是,保持一致.
一致性的代碼更容易維護、是更合理的、需要更少的學習成本、並且隨着新的約定出現或者出現錯誤后更容易遷移、更新、修復 bug
相反,一個單一的代碼庫會導致維護成本開銷、不確定性和認知偏差。所有這些都會直接導致速度降低、
代碼審查痛苦、而且增加 bug 數量
將這些標準應用於代碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程序通過將多個樣式引入到同一代碼中,違反了上述關注點。
相似的聲明放在一組
Go 語言支持將相似的聲明放在一個組內。
Bad | Good |
---|---|
“`go import “a” import “b” “` | “`go import ( “a” “b” ) “` |
這同樣適用於常量、變量和類型聲明:
Bad | Good |
---|---|
“`go const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 “` | “`go const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) “` |
僅將相關的聲明放在一組。不要將不相關的聲明放在一組。
Bad | Good |
---|---|
“`go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ENV_VAR = “MY_ENV” ) “` | “`go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const ENV_VAR = “MY_ENV” “` |
分組使用的位置沒有限制,例如:你可以在函數內部使用它們:
Bad | Good |
---|---|
“`go func f() string { var red = color.New(0xff0000) var green = color.New(0x00ff00) var blue = color.New(0x0000ff) … } “` | “`go func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) … } “` |
import 分組
導入應該分為兩組:
- 標準庫
- 其他庫
默認情況下,這是 goimports 應用的分組。
Bad | Good |
---|---|
“`go import ( “fmt” “os” “go.uber.org/atomic” “golang.org/x/sync/errgroup” ) “` | “`go import ( “fmt” “os” “go.uber.org/atomic” “golang.org/x/sync/errgroup” ) “` |
包名
當命名包時,請按下面規則選擇一個名稱:
- 全部小寫。沒有大寫或下劃線。
- 大多數使用命名導入的情況下,不需要重命名。
- 簡短而簡潔。請記住,在每個使用的地方都完整標識了該名稱。
- 不用複數。例如
net/url
,而不是net/urls
。 - 不要用“common”,“util”,“shared”或“lib”。這些是不好的,信息量不足的名稱。
另請參閱 和 .
函數名
我們遵循 Go 社區關於使用 的約定。有一個例外,為了對相關的測試用例進行分組,函數名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested
.
導入別名
如果程序包名稱與導入路徑的最後一個元素不匹配,則必須使用導入別名。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
在所有其他情況下,除非導入之間有直接衝突,否則應避免導入別名。
Bad | Good |
---|---|
“`go import ( “fmt” “os” nettrace “golang.net/x/trace” ) “` | “`go import ( “fmt” “os” “runtime/trace” nettrace “golang.net/x/trace” ) “` |
函數分組與順序
- 函數應按粗略的調用順序排序。
- 同一文件中的函數應按接收者分組。
因此,導出的函數應先出現在文件中,放在struct
, const
, var
定義的後面。
在定義類型之後,但在接收者的其餘方法之前,可能會出現一個 newXYZ()
/NewXYZ()
由於函數是按接收者分組的,因此普通工具函數應在文件末尾出現。
Bad | Good |
---|---|
“`go func (s *something) Cost() { return calcCost(s.weights) } type something struct{ … } func calcCost(n []int) int {…} func (s *something) Stop() {…} func newSomething() *something { return &something{} } “` | “`go type something struct{ … } func newSomething() *something { return &something{} } func (s *something) Cost() { return calcCost(s.weights) } func (s *something) Stop() {…} func calcCost(n []int) int {…} “` |
減少嵌套
代碼應通過盡可能先處理錯誤情況/特殊情況並儘早返回或繼續循環來減少嵌套。減少嵌套多個級別的代碼的代碼量。
Bad | Good |
---|---|
“`go for _, v := range data { if v.F1 == 1 { v = process(v) if err := v.Call(); err == nil { v.Send() } else { return err } } else { log.Printf(“Invalid v: %v”, v) } } “` | “`go for _, v := range data { if v.F1 != 1 { log.Printf(“Invalid v: %v”, v) continue } v = process(v) if err := v.Call(); err != nil { return err } v.Send() } “` |
不必要的 else
如果在 if 的兩個分支中都設置了變量,則可以將其替換為單個 if。
Bad | Good |
---|---|
“`go var a int if b { a = 100 } else { a = 10 } “` | “`go a := 10 if b { a = 100 } “` |
頂層變量聲明
在頂層,使用標準var
關鍵字。請勿指定類型,除非它與表達式的類型不同。
Bad | Good |
---|---|
“`go var _s string = F() func F() string { return “A” } “` | “`go var _s = F() // 由於 F 已經明確了返回一個字符串類型,因此我們沒有必要顯式指定_s 的類型 // 還是那種類型 func F() string { return “A” } “` |
如果表達式的類型與所需的類型不完全匹配,請指定類型。
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 |
---|---|
“`go // foo.go const ( defaultPort = 8080 defaultUser = “user” ) // bar.go func Bar() { defaultPort := 9090 … fmt.Println(“Default port”, defaultPort) // We will not see a compile error if the first line of // Bar() is deleted. } “` | “`go // foo.go const ( _defaultPort = 8080 _defaultUser = “user” ) “` |
結構體中的嵌入
嵌入式類型(例如 mutex)應位於結構體內的字段列表的頂部,並且必須有一個空行將嵌入式字段與常規字段分隔開。
Bad | Good |
---|---|
“`go type Client struct { version int http.Client } “` | “`go type Client struct { http.Client version int } “` |
使用字段名初始化結構體
初始化結構體時,幾乎始終應該指定字段名稱。現在由 強制執行。
Bad | Good |
---|---|
“`go k := User{“John”, “Doe”, true} “` | “`go k := User{ FirstName: “John”, LastName: “Doe”, Admin: true, } “` |
例外:如果有 3 個或更少的字段,則可以在測試表中省略字段名稱。
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
本地變量聲明
如果將變量明確設置為某個值,則應使用短變量聲明形式 (:=
)。
Bad | Good |
---|---|
“`go var s = “foo” “` | “`go s := “foo” “` |
但是,在某些情況下,var
使用關鍵字時默認值會更清晰。例如,聲明空切片。
Bad | Good |
---|---|
“`go func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } “` | “`go func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } “` |
nil 是一個有效的 slice
nil
是一個有效的長度為 0 的 slice,這意味着,
-
您不應明確返回長度為零的切片。應該返回
nil
來代替。Bad Good “`go if x == “” { return []int{} } “` “`go if x == “” { return nil } “` -
要檢查切片是否為空,請始終使用
len(s) == 0
。而非nil
。Bad Good “`go func isEmpty(s []string) bool { return s == nil } “` “`go func isEmpty(s []string) bool { return len(s) == 0 } “` -
零值切片(用
var
聲明的切片)可立即使用,無需調用make()
創建。Bad Good “`go nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } “` “`go var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } “`
小變量作用域
如果有可能,盡量縮小變量作用範圍。除非它與 的規則衝突。
Bad | Good |
---|---|
“`go err := ioutil.WriteFile(name, data, 0644) if err != nil { return err } “` | “`go if err := ioutil.WriteFile(name, data, 0644); err != nil { return err } “` |
如果需要在 if 之外使用函數調用的結果,則不應嘗試縮小範圍。
Bad | Good |
---|---|
“`go if data, err := ioutil.ReadFile(name); err == nil { err = cfg.Decode(data) if err != nil { return err } fmt.Println(cfg) return nil } else { return err } “` | “`go data, err := ioutil.ReadFile(name) if err != nil { return err } if err := cfg.Decode(data); err != nil { return err } fmt.Println(cfg) return nil “` |
避免參數語義不明確(Avoid Naked Parameters)
函數調用中的意義不明確的參數
可能會損害可讀性。當參數名稱的含義不明顯時,請為參數添加 C 樣式註釋 (/* ... */
)
Bad | Good |
---|---|
“`go // func printInfo(name string, isLocal, done bool) printInfo(“foo”, true, true) “` | “`go // func printInfo(name string, isLocal, done bool) printInfo(“foo”, true /* isLocal */, true /* done */) “` |
對於上面的示例代碼,還有一種更好的處理方式是將上面的 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 |
---|---|
“`go wantError := “unknown name:\”test\”” “` | “`go wantError := `unknown error:”test”` “` |
初始化 Struct 引用
在初始化結構引用時,請使用&T{}
代替new(T)
,以使其與結構體初始化一致。
Bad | Good |
---|---|
“`go sval := T{Name: “foo”} // inconsistent sptr := new(T) sptr.Name = “bar” “` | “`go sval := T{Name: “foo”} sptr := &T{Name: “bar”} “` |
初始化 Maps
對於空 map 請使用 make(..)
初始化, 並且 map 是通過編程方式填充的。
這使得 map 初始化在表現上不同於聲明,並且它還可以方便地在 make 后添加大小提示。
Bad | Good |
---|---|
“`go var ( // m1 讀寫安全; // m2 在寫入時會 panic m1 = map[T1]T2{} m2 map[T1]T2 ) “` | “`go var ( // m1 讀寫安全; // m2 在寫入時會 panic m1 = make(map[T1]T2) m2 map[T1]T2 ) “` |
聲明和初始化看起來非常相似的。 | 聲明和初始化看起來差別非常大。 |
在盡可能的情況下,請在初始化時提供 map 容量大小,詳細請看 。
另外,如果 map 包含固定的元素列表,則使用 map literals(map 初始化列表) 初始化映射。
Bad | Good |
---|---|
“`go m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 “` | “`go m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } “` |
基本準則是:在初始化時使用 map 初始化列表 來添加一組固定的元素。否則使用 make
(如果可以,請盡量指定 map 容量)。
字符串 string format
如果你為Printf
-style 函數聲明格式字符串,請將格式化字符串放在外面,並將其設置為const
常量。
這有助於go vet
對格式字符串執行靜態分析。
Bad | Good |
---|---|
“`go msg := “unexpected values %v, %v\n” fmt.Printf(msg, 1, 2) “` | “`go const msg = “unexpected values %v, %v\n” fmt.Printf(msg, 1, 2) “` |
命名 Printf 樣式的函數
聲明Printf
-style 函數時,請確保go vet
可以檢測到它並檢查格式字符串。
這意味着您應盡可能使用預定義的Printf
-style 函數名稱。go vet
將默認檢查這些。有關更多信息,請參見 。
如果不能使用預定義的名稱,請以 f 結束選擇的名稱:Wrapf
,而不是Wrap
。go vet
可以要求檢查特定的 Printf 樣式名稱,但名稱必須以f
結尾。
$ go vet -printfuncs=wrapf,statusf
另請參閱 .
編程模式
表驅動測試
當測試邏輯是重複的時候,通過 使用 table 驅動的方式編寫 case 代碼看上去會更簡潔。
Bad | Good |
---|---|
“`go // func TestSplitHostPort(t *testing.T) host, port, err := net.SplitHostPort(“192.0.2.0:8000”) require.NoError(t, err) assert.Equal(t, “192.0.2.0”, host) assert.Equal(t, “8000”, port) host, port, err = net.SplitHostPort(“192.0.2.0:http”) require.NoError(t, err) assert.Equal(t, “192.0.2.0”, host) assert.Equal(t, “http”, port) host, port, err = net.SplitHostPort(“:8000”) require.NoError(t, err) assert.Equal(t, “”, host) assert.Equal(t, “8000”, port) host, port, err = net.SplitHostPort(“1:8”) require.NoError(t, err) assert.Equal(t, “1”, host) assert.Equal(t, “8”, port) “` | “`go // func TestSplitHostPort(t *testing.T) tests := []struct{ give string wantHost string wantPort string }{ { give: “192.0.2.0:8000”, wantHost: “192.0.2.0”, wantPort: “8000”, }, { give: “192.0.2.0:http”, wantHost: “192.0.2.0”, wantPort: “http”, }, { give: “:8000”, wantHost: “”, wantPort: “8000”, }, { give: “1:8”, wantHost: “1”, wantPort: “8”, }, } for _, tt := range tests { t.Run(tt.give, func(t *testing.T) { host, port, err := net.SplitHostPort(tt.give) require.NoError(t, err) assert.Equal(t, tt.wantHost, host) assert.Equal(t, tt.wantPort, port) }) } “` |
很明顯,使用 test table 的方式在代碼邏輯擴展的時候,比如新增 test case,都會顯得更加的清晰。
我們遵循這樣的約定:將結構體切片稱為tests
。 每個測試用例稱為tt
。此外,我們鼓勵使用give
和want
前綴說明每個測試用例的輸入和輸出值。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
功能選項
功能選項是一種模式,您可以在其中聲明一個不透明 Option 類型,該類型在某些內部結構中記錄信息。您接受這些選項的可變編號,並根據內部結構上的選項記錄的全部信息採取行動。
將此模式用於您需要擴展的構造函數和其他公共 API 中的可選參數,尤其是在這些功能上已經具有三個或更多參數的情況下。
Bad | Good |
---|---|
“`go // package db func Connect( addr string, timeout time.Duration, caching bool, ) (*Connection, error) { // … } // Timeout and caching must always be provided, // even if the user wants to use the default. db.Connect(addr, db.DefaultTimeout, db.DefaultCaching) db.Connect(addr, newTimeout, db.DefaultCaching) db.Connect(addr, db.DefaultTimeout, false /* caching */) db.Connect(addr, newTimeout, false /* caching */) “` | “`go type options struct { timeout time.Duration caching bool } // Option overrides behavior of Connect. type Option interface { apply(*options) } type optionFunc func(*options) func (f optionFunc) apply(o *options) { f(o) } func WithTimeout(t time.Duration) Option { return optionFunc(func(o *options) { o.timeout = t }) } func WithCaching(cache bool) Option { return optionFunc(func(o *options) { o.caching = cache }) } // Connect creates a connection. func Connect( addr string, opts …Option, ) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, } for _, o := range opts { o.apply(&options) } // … } // Options must be provided only if needed. db.Connect(addr) db.Connect(addr, db.WithTimeout(newTimeout)) db.Connect(addr, db.WithCaching(false)) db.Connect( addr, db.WithCaching(false), db.WithTimeout(newTimeout), ) “` |
還可以參考下面資料:
本文由zshipu.com學習筆記或整理或轉載,如有侵權請聯繫,必改之。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線
※廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益
※自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象
※南投搬家前需注意的眉眉角角,別等搬了再說!