- 原文地址:https://github.com/uber-go/guide/blob/master/style.md
- 譯文出處:https://github.com/uber-go/guide
- 本文永久連接:https://github.com/gocn/translator/blob/master/2019/w38_uber_go_style_guide.md
- 譯者:咔嘰咔嘰
- 校對者:fivezh,cvley
目錄
- 介紹
- 指南
- 接口的指針
- 接收者和接口
- 零值 Mutexes 是有效的
- 複製 Slice 和 Map
- Defer 的使用
- channel 的大小是 1 或者 None
- 枚舉值從 1 開始
- Error 類型
- Error 包裝
- 處理類型斷言失敗
- 避免 Panic
- 使用 go.uber.org/atomic
- 性能
- strconv 優於 fmt
- 避免 string 到 byte 的轉換
- 代碼樣式
- 聚合類似的聲明
- 包的分組導入的順序
- 包命名
- 函數命名
- 別名導入
- 函數分組和順序
- 減小嵌套
- 沒必要要的 else
- 頂層變量的聲明
- 在不可導出的全局變量前面加上 _
- 結構體的嵌入
- 使用字段名去初始化結構體
- 局部變量聲明
- nil 是一個有效的 slice
- 減小變量的做用域
- 避免裸參數
- 使用原生字符串格式來避免轉義
- 初始化結構體
- 在 Printf 以外格式化字符串
- Printf-style 函數的命名
- 設計模式
- 表格驅動測試
- 函數參數可選化
介紹
代碼風格是代碼的一種約定。用風格這個詞可能有點不恰當,由於這些約定涉及到的遠比源碼文件格式工具 gofmt 所能處理的更多。html
本指南的目標是經過詳細描述 Uber 在編寫 Go 代碼時的取捨來管理代碼的這種複雜性。這些規則的存在是爲了保持代碼庫的可管理性,同時也容許工程師更高效地使用 go 語言特性。git
本指南最初由 Prashant Varanasi 和 Simon Newton 爲了讓同事們更便捷地使用 go 語言而編寫。多年來根據其餘人的反饋進行了一些修改。github
本文記錄了 uber 在使用 go 代碼中的一些習慣用法。許多都是 go 語言常見的指南,而其餘的則延伸到了一些外部資料:golang
所用的代碼在運行 golint
和 go vet
以後不會有報錯。建議將編輯器設置爲:shell
- 保存時運行 goimports
- 運行
golint
和go vet
來檢查錯誤
你能夠在下面的連接找到 Go tools 對一些編輯器的支持:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins設計模式
指南
接口的指針
你幾乎不須要指向接口的指針,應該把接口看成值傳遞,它的底層數據仍然能夠當成一個指針。api
一個接口是兩個字段:安全
- 指向特定類型信息的指針。你能夠認爲這是 "type."。
- 若是存儲的數據是指針,則直接存儲。若是數據存儲的是值,則存儲指向此值的指針。
若是你但願接口方法修改底層數據,則必須使用指針。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[0].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 是一個值,而且 f 沒有值接收者 // i = s2Val
Effective Go 對 Pointers vs. Values 分析的不錯.
零值 Mutexes 是有效的
零值的 sync.Mutex
和 sync.RWMutex
是有效的,因此你幾乎不須要指向 mutex 的指針。
Bad | Good |
---|---|
mu := new(sync.Mutex) |
mu.Lock() var mu sync.Mutex mu.Lock()
|
var mu sync.Mutex mu.Lock() defer mu.Unlock()
若是你使用一個指針指向的結構體,mutex 能夠做爲一個非指針字段,或者,最好是直接嵌入這個結構體。
type smap struct { sync.Mutex 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] }
|
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] }
|
爲私有類型或須要實現 Mutex 接口的類型嵌入 | 對於導出的類型,使用私有鎖。 |
複製 Slice 和 Map
slice 和 map 包含指向底層數據的指針,所以複製的時候須要小心。
接收 Slice 和 Map 做爲入參
須要留意的是,若是你保存了做爲參數接收的 map 或 slice 的引用,能夠經過引用修改它。
Bad | Good |
---|---|
func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // Did you mean to modify d1.trips? trips[0] = ...
|
func (d *Driver) SetTrips(trips []Trip) { d.trips = make([]Trip, len(trips)) copy(d.trips, trips) } trips := ... d1.SetTrips(trips) // We can now modify trips[0] without affecting d1.trips. trips[0] = ...
|
返回 Slice 和 Map
相似的,小心 map 或者 slice 暴露的內部狀態是能夠被修改的。
Bad | Good |
---|---|
type Stats struct { sync.Mutex counters map[string]int } // Snapshot 方法返回當前的狀態 func (s *Stats) Snapshot() map[string]int { s.Lock() defer s.Unlock() return s.counters } // snapshot 再也不被鎖保護 snapshot := stats.Snapshot()
|
type Stats struct { sync.Mutex counters map[string]int } func (s *Stats) Snapshot() map[string]int { s.Lock() defer s.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 |
---|---|
p.Lock() if p.count < 10 { p.Unlock() return p.count } p.count++ newCount := p.count p.Unlock() return newCount // 多個返回語句致使很容易忘記釋放鎖
|
p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // 更可讀
|
defer 的開銷很是小,只有在你以爲你的函數執行須要在納秒級別的狀況下才須要考慮避免使用。使用 defer 換取的可讀性是值得的。這尤爲適用於具備比簡單內存訪問更復雜的大型方法,這時其餘的計算比 defer 更重要。
channel 的大小是 1 或者 None
channel 的大小一般應該是 1 或者是無緩衝的。默認狀況下,channel 是無緩衝的且大小爲 0。任何其餘的大小都必須通過仔細檢查。應該考慮如何肯定緩衝的大小,哪些因素能夠防止 channel 在負載時填滿和阻塞寫入,以及當這種狀況發生時會形成什麼樣的影響。
Bad | Good |
---|---|
// Ought to be enough for anybody! c := make(chan int, 64)
|
// size 爲 1 c := make(chan int, 1) // 或者 // 非緩衝 channel,size 爲 0 c := make(chan int)
|
枚舉值從 1 開始
在 Go 中引入枚舉的標準方法是聲明一個自定義類型和一個帶 iota
的 const
組。因爲變量的默認值爲 0,所以一般應該以非零值開始枚舉。
Bad | Good |
---|---|
type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2
|
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
Error 類型
聲明 error 有多種選項:
errors.New
聲明簡單靜態的字符串fmt.Errorf
聲明格式化的字符串- 實現了
Error()
方法的自定義類型 - 使用
"pkg/errors".Wrap
包裝 error
返回 error 時,能夠考慮如下因素以肯定最佳選擇:
- 不須要額外信息的一個簡單的 error? 那麼
errors.New
就夠了 - 客戶端須要檢查並處理這個 error?那麼應該使用實現了
Error()
方法的自定義類型 - 是否須要傳遞下游函數返回的 error?那麼請看 section on error wrapping
- 不然, 可使用
fmt.Errorf
若是客戶端須要檢查這個 error,你須要使用 errors.New
和 var 來建立一個簡單的 error。
Bad | Good |
---|---|
// 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") } } }
|
// 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") } }
|
若是你有一個 error 可能須要客戶端去檢查,而且你想增長更多的信息(例如,它不是一個簡單的靜態字符串),這時候你須要使用自定義類型。
Bad | Good |
---|---|
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") } } }
|
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") } } }
|
在直接導出自定義 error 類型的時候須要當心,由於它已是包的公共 API。最好暴露一個 matcher 函數(譯者注:如下示例的 IsNotFoundError 函數)去檢查 error。
// 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 包裝
若是調用失敗,有三個主要選項用於 error 傳遞:
- 若是沒有額外增長的上下文而且你想維持原始 error 類型,那麼返回原始 error
- 使用
"pkg/errors".Wrap
增長上下文,以致於 error 信息提供更多的上下文,而且"pkg/errors".Cause
能夠用來提取原始 error - 若是調用者不須要檢查或者處理具體的 error 例子,那麼使用
fmt.Errorf
推薦去增長上下文信息取代描述模糊的 error,例如 "connection refused",應該返回例如 "failed to call service foo: connection refused" 這樣更有用的 error。
請參考 Don't just check errors, handle them gracefully.
處理類型斷言失敗
簡單的返回值形式的類型斷言在斷言不正確的類型時將會 panic。所以,須要使用 ", ok" 的經常使用方式。
Bad | Good |
---|---|
t := i.(string)
|
t, ok := i.(string) if !ok { // handle the error gracefully }
|
避免 Panic
生產環境跑的代碼必須避免 panic。它是致使 級聯故障 的主要緣由。若是一個 error 產生了,函數必須返回 error 而且容許調用者決定是否處理它。
Bad | Good |
---|---|
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 <bar>") os.Exit(1) } foo(os.Args[1]) }
|
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 <bar>") os.Exit(1) } if err := foo(os.Args[1]); err != nil { panic(err) } }
|
panic/recover 不是 error 處理策略。程序在發生不可恢復的時候會產生 panic,例如對 nil 進行解引用。一個例外是在程序初始化的時候:在程序啓動時那些可能終止程序的問題會形成 panic。
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
甚至在測試用例中,更偏向於使用 或者 解決 panic 確保這個測試被標記爲失敗。t.Fatalt.FailNow
Bad | Good |
---|---|
// func TestFoo(t *testing.T) f, err := ioutil.TempFile("", "test") if err != nil { panic("failed to set up test") }
|
// func TestFoo(t *testing.T) f, err := ioutil.TempFile("", "test") if err != nil { t.Fatal("failed to set up test") }
|
使用 go.uber.org/atomic
使用 sync/atomic 對原生類型(例如,int32
,int64
)進行原子操做的時候,很容易在讀取或者修改變量的時候忘記使用原子操做。
go.uber.org/atomic 經過隱藏底層類型使得這些操做是類型安全的。此外,它還包含一個比較方便的 atomic.Bool
類型。
Bad | Good |
---|---|
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! }
|
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() }
|
性能
指定的性能指南僅適用於 hot path(譯者注:hot path 指頻繁執行的代碼路徑)
strconv 優於 fmt
對基本數據類型的字符串表示的轉換,strconv
比 fmt
速度快。
Bad | Good |
---|---|
var i int = ... s := fmt.Sprint(i)
|
var i int = ... s := strconv.Itoa(i)
|
避免 string 到 byte 的轉換
不要重複用固定 string 建立 byte slice。相反,執行一次轉換後保存結果,避免重複轉換。
Bad | Good |
---|---|
for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) }
|
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 |
代碼風格
聚合類似的聲明
Go 支持分組聲明。
Bad | Good |
---|---|
import "a" import "b"
|
import ( "a" "b" )
|
也能應用於常量,變量和類型的聲明。
Bad | Good |
---|---|
const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64
|
const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 )
|
只須要對相關類型進行分組聲明。不相關的不須要進行分組聲明。
Bad | Good |
---|---|
type Operation int const ( Add Operation = iota + 1 Subtract Multiply ENV_VAR = "MY_ENV" )
|
type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const ENV_VAR = "MY_ENV"
|
分組不受限制。例如,咱們能夠在函數內部使用它們。
Bad | Good |
---|---|
func f() string { var red = color.New(0xff0000) var green = color.New(0x00ff00) var blue = color.New(0x0000ff) ... }
|
func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) ... }
|
包的分組導入的順序
有兩個導入分組:
- 標準庫
- 其餘庫
這是默認狀況下 goimports 應用的分組。
Bad | Good |
---|---|
import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" )
|
import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" )
|
包命名
當給包命名的時候,能夠參考如下方法,
- 都是小寫字母。沒有大寫字母或者下劃線
- 在大多數場景下不必重命名包
- 簡明扼要。記住,每次調用時都會經過名稱來識別。
- 不要複數。例如,要使用
net/url
, 不要使用net/urls
- 不要使用 "common", "util", "shared", "lib" 諸如此類的命名。這種方式不太好,沒法從名字中獲取有效信息。
也能夠參考 Package Names 和 Style guideline for Go packages.
函數命名
咱們遵循 Go 社區的習慣方法,使用駝峯法命名函數。測試函數是個例外,包含下劃線是爲了分組相關的測試用例。例如,TestMyFunction_WhatIsBeingTested
。
別名導入
若是包名和導入路徑的最後一個元素不匹配,則要使用別名導入。
import ( "net/http" client "example.com/client-go" trace "example.com/trace/v2" )
在大部分場景下,除非導入的包有直接的衝突,應該避免使用別名導入。
Bad | Good |
---|---|
import ( "fmt" "os" nettrace "golang.net/x/trace" )
|
import ( "fmt" "os" "runtime/trace" nettrace "golang.net/x/trace" )
|
函數分組和順序
- 函數應該按大體的調用順序排序
- 同一個文件的函數應該按接收者分組
所以,導出的函數應該在 struct
,const
,var
定義以後。
newXYZ()
/NewXYZ()
應該在類型定義以後,而且在接收者的其他的方法以前出現。
由於函數是按接收者分組的,因此普通的函數應該快到文件末尾了。
Bad | Good |
---|---|
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{} }
|
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 |
---|---|
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) } }
|
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 |
---|---|
var a int if b { a = 100 } else { a = 10 }
|
a := 10 if b { a = 100 }
|
頂層變量的聲明
在頂層,使用標準的 var
關鍵字。不要指定類型,除非它與表達式的類型不一樣。
Bad | Good |
---|---|
var _s string = F() func F() string { return "A" }
|
var _s = F() // F 已經聲明瞭返回一個 string,咱們不須要再次指定類型 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
在不可導出的全局變量前面加上 _
在不可導出的頂層 var
和 const
的前面加上 _
,以便明確它們是全局符號。
特例:不可導出的 error 值前面應該加上 err
前綴。
理論依據:頂層變量和常量有一個包做用域。使用通用的名稱很容易在不一樣的文件中意外地使用錯誤的值
Bad | Good |
---|---|
// foo.go const ( defaultPort = 8080 defaultUser = "user" ) // bar.go func Bar() { defaultPort := 9090 ... fmt.Println("Default port", defaultPort) // 咱們將 Bar() 的第一行刪除,將不會看到編譯錯誤 }
|
// foo.go const ( _defaultPort = 8080 _defaultUser = "user" )
|
結構體的嵌入
嵌入的類型(例如 mutex)應該在結構體字段的頭部,而且在嵌入字段和常規字段間保留一個空行來隔離。
Bad | Good |
---|---|
type Client struct { version int http.Client }
|
type Client struct { http.Client version int }
|
使用字段名去初始化結構體
當初始化結構體的時候應該指定字段名稱,如今在使用 go vet
的狀況下是強制性的。
Bad | Good |
---|---|
k := User{"John", "Doe", true}
|
k := User{ FirstName: "John", LastName: "Doe", Admin: true, }
|
特例:當有 3 個或更少的字段時,能夠在測試表中省略字段名。
tests := []struct{ }{ op Operation want string }{ {Add, "add"}, {Subtract, "subtract"}, }
局部變量聲明
短變量聲明(:=
)應該被使用在有明確值的狀況下。
Bad | Good |
---|---|
var s = "foo"
|
s := "foo"
|
然而,使用 var
關鍵字在某些狀況下會讓默認值更清晰,聲明空 Slice,例如
Bad | Good |
---|---|
func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } }
|
func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } }
|
nil 是一個有效的 slice
nil
是一個長度爲 0 的 slice。意思是,
-
使用
nil
來替代長度爲 0 的 slice 返回Bad Good if x == "" { return []int{} }
if x == "" { return nil }
-
檢查一個空 slice,應該使用
len(s) == 0
,而不是nil
。Bad Good func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
-
The zero value (a slice declared with
var
) is usable immediately withoutmake()
. -
零值(經過
var
聲明的 slice)是立馬可用的,並不須要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 |
---|---|
err := f.Close() if err != nil { return err }
|
if err := f.Close(); err != nil { return err }
|
若是在 if 以外須要函數調用的結果,則不要縮小做用域。
Bad | Good |
---|---|
if f, err := os.Open("f"); err == nil { _, err = io.WriteString(f, "data") if err != nil { return err } return f.Close() } else { return err }
|
f, err := os.Open("f") if err != nil { return err } if _, err := io.WriteString(f, "data"); err != nil { return err } return f.Close()
|
避免裸參數
函數調用中的裸參數不利於可讀性。當參數名的含義不明顯時,添加 C 語言風格的註釋(/*…*/
)。
Bad | Good |
---|---|
// func printInfo(name string, isLocal, done bool) printInfo("foo", true, true)
|
// 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 // 可能將來咱們將有一個 StatusInProgress 的狀態 ) func printInfo(name string, region Region, status Status)
使用原生字符串格式來避免轉義
Go 支持 原生字符串格式 ,它能夠跨越多行幷包含引號。使用這些來避免手動轉義的字符串,由於手動轉義的可讀性不好。
Bad | Good |
---|---|
wantError := "unknown name:\"test\""
|
wantError := `unknown error:"test"`
|
初始化結構體
在初始化結構體的時候使用 &T{}
替代 new(T)
,以致於結構體初始化是一致的。
Bad | Good |
---|---|
sval := T{Name: "foo"} // 不一致 sptr := new(T) sptr.Name = "bar"
|
sval := T{Name: "foo"} sptr := &T{Name: "bar"}
|
在 Printf 以外格式化字符串
若是你在 Printf
風格函數的外面聲明一個格式化字符串,請使用 const
值。
這有助於 go vet
對格式化字符串執行靜態分析。
Bad | Good |
---|---|
msg := "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2)
|
const msg = "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2)
|
Printf-style 函數的命名
當你聲明一個 Printf
風格的函數,請確認 go vet
可以發現並檢查這個格式化字符串。
這意味着你應該儘量爲 Printf
風格的函數名進行預約義 。go vet
默認會檢查它們。查看 Printf family 獲取更多信息。
若是預約義函數名不可取,請用 f 做爲名字的後綴即 wrapf
,而不是 wrap
。go vet
能夠檢查特定的 printf
風格的名稱,但它們必須以 f 結尾。
$ go vet -printfuncs=wrapf,statusf
請參考 go vet: Printf family check。
設計模式
表格驅動測試
當核心測試邏輯重複的時候,用 subtests 作表格驅動測試(譯者注:table-driven tests 即 TDT 表格驅動方法)能夠避免重複的代碼。
Bad | Good |
---|---|
// 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)
|
// 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) }) }
|
表格驅動測試使向錯誤消息添加上下文、減小重複邏輯和添加新測試用例變得更容易。
咱們遵循這樣一種約定,即結構體 slice 被稱爲 tests
,每一個測試用例被稱爲 tt
。此外,咱們鼓勵使用 give
和 want
前綴解釋每一個測試用例的輸入和輸出值。
tests := []struct{ give string wantHost string wantPort string }{ // ... } for _, tt := range tests { // ... }
函數參數可選化
函數參數可選化(functional options)是一種模式,在這種模式中,你能夠聲明一個不肯定的 Option
類型,該類型在內部結構體中記錄信息。函數接收可選化的參數,並根據在結構體上記錄的參數信息進行操做
將此模式用於構造函數和其餘須要擴展的公共 API 中的可選參數,特別是在這些函數上已經有三個或更多參數的狀況下。
Bad | Good |
---|---|
// package db func Connect( addr string, timeout time.Duration, caching bool, ) (*Connection, error) { // ... } // timeout 和 caching 必需要提供,哪怕用戶想使用默認值 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 */)
|
type options struct { timeout time.Duration caching bool } // Option 重寫 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 建立一個 connection func Connect( addr string, opts ...Option, ) (*Connection, error) { options := options{ timeout: defaultTimeout, caching: defaultCaching, } for _, o := range opts { o.apply(&options) } // ... } // Options 只在須要的時候提供 db.Connect(addr) db.Connect(addr, db.WithTimeout(newTimeout)) db.Connect(addr, db.WithCaching(false)) db.Connect( addr, db.WithCaching(false), db.WithTimeout(newTimeout), )
|
請參考,