Uber 是一家美國硅谷的科技公司,也是 Go 語言的早期 adopter。其開源了不少 golang 項目,諸如被 Gopher 圈熟知的 zap、jaeger 等。2018 年年底 Uber 將內部的 Go 風格規範 開源到 GitHub,通過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注。本文是該規範的中文版本。本版本會根據原版實時更新。html
## 版本git
樣式 (style) 是支配咱們代碼的慣例。術語樣式
有點用詞不當,由於這些約定涵蓋的範圍不限於由 gofmt 替咱們處理的源文件格式。github
本指南的目的是經過詳細描述在 Uber 編寫 Go 代碼的注意事項來管理這種複雜性。這些規則的存在是爲了使代碼庫易於管理,同時仍然容許工程師更有效地使用 Go 語言功能。golang
該指南最初由 Prashant Varanasi 和 Simon Newton 編寫,目的是使一些同事能快速使用 Go。多年來,該指南已根據其餘人的反饋進行了修改。shell
本文檔記錄了咱們在 Uber 遵循的 Go 代碼中的慣用約定。其中許可能是 Go 的通用準則,而其餘擴展準則依賴於下面外部的指南:編程
全部代碼都應該經過golint
和go vet
的檢查並沒有錯誤。咱們建議您將編輯器設置爲:c#
goimports
golint
和 go vet
檢查錯誤您能夠在如下 Go 編輯器工具支持頁面中找到更爲詳細的信息:
https://github.com/golang/go/wiki/IDEsAndTextEditorPluginsapi
您幾乎不須要指向接口類型的指針。您應該將接口做爲值進行傳遞,在這樣的傳遞過程當中,實質上傳遞的底層數據仍然能夠是指針。安全
接口實質上在底層用兩個字段表示:app
若是但願接口方法修改基礎數據,則必須使用指針傳遞。
使用值接收器的方法既能夠經過值調用,也能夠經過指針調用。
例如,
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 的精彩講解。
零值 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 包含了指向底層數據的指針,所以在須要複製它們時要特別注意。
請記住,當 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] = ... ``` |
一樣,請注意用戶對暴露內部狀態的 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 釋放資源,諸如文件和鎖。
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 爲零。任何其餘尺寸都必須通過嚴格的審查。考慮如何肯定大小,是什麼阻止了 channel 在負載下被填滿並阻止寫入,以及發生這種狀況時發生了什麼。
Bad | Good |
---|---|
```go // 應該足以知足任何狀況! c := make(chan int, 64) ``` | ```go // 大小:1 c := make(chan int, 1) // 或者 // 無緩衝 channel,大小爲 0 c := make(chan int) ``` |
在 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) 的選項:
errors.New
對於簡單靜態字符串的錯誤fmt.Errorf
用於格式化的錯誤字符串Error()
方法的自定義類型"pkg/errors".Wrap
的 Wrapped errors返回錯誤時,請考慮如下因素以肯定最佳選擇:
errors.New
足夠了。Error()
方法。fmt.Errorf
就能夠了。若是客戶端須要檢測錯誤,而且您已使用建立了一個簡單的錯誤 errors.New
,請使用一個錯誤變量。
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") } }
一個(函數/方法)調用失敗時,有三種主要的錯誤傳播方式:
添加上下文,使用 "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 |
---|---|
```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」爲前綴)。
另請參見 Don't just check errors, handle them gracefully. 不要只是檢查錯誤,要優雅地處理錯誤
type assertion 的單個返回值形式針對不正確的類型將產生 panic。所以,請始終使用「comma ok」的慣用法。
Bad | Good |
---|---|
```go t := i.(string) ``` | ```go t, ok := i.(string) if !ok { // 優雅地處理錯誤 } ``` |
在生產環境中運行的代碼必須避免出現 panic。panic 是 cascading failures 級聯失敗的主要根源 。若是發生錯誤,該函數必須返回錯誤,並容許調用方決定如何處理它。
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
|
```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
|
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") } ``` |
使用 sync/atomic 包的原子操做對原始類型 (int32
, int64
等)進行操做,由於很容易忘記使用原子操做來讀取或修改變量。
go.uber.org/atomic 經過隱藏基礎類型爲這些操做增長了類型安全性。此外,它包括一個方便的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
快。
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 ``` |
在儘量的狀況下,在使用 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) ) ... } ``` |
導入應該分爲兩組:
默認狀況下,這是 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
。另請參閱 Package Names 和 Go 包樣式指南.
咱們遵循 Go 社區關於使用 MixedCaps 做爲函數名 的約定。有一個例外,爲了對相關的測試用例進行分組,函數名可能包含下劃線,如: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() } ``` |
若是在 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 } ``` |
初始化結構體時,幾乎始終應該指定字段名稱。如今由 go vet
強制執行。
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
是一個有效的長度爲 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 ``` |
函數調用中的意義不明確的參數
可能會損害可讀性。當參數名稱的含義不明顯時,請爲參數添加 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"` ``` |
在初始化結構引用時,請使用&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"} ``` |
對於空 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 包含固定的元素列表,則使用 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 容量)。
若是你爲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
-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 |
---|---|
```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學習筆記或整理或轉載,若有侵權請聯繫,必改之。