相信很多人前兩天都看到 Uber 在 github 上面開源的 Go 語言編程規范了,原文在這里:https://github.com/uber-go/guide/blob/master/style.md 。我們今天就來簡單了解一下國外大廠都是如何來寫代碼的。行文倉促,錯誤之處,多多指正。另外如果覺得還不錯,也歡迎分享給更多的人。
[TOC]
1. 介紹
英文原文標題是 Uber Go Style Guide,這里的 Style 是指在管理我們代碼的時候可以遵從的一些約定。
這篇編程指南的初衷是更好的管理我們的代碼,包括去編寫什么樣的代碼,以及不要編寫什么樣的代碼。我們希望通過這份編程指南,代碼可以具有更好的維護性,同時能夠讓我們的開發同學更高效地編寫 Go 語言代碼。
這份編程指南最初由 Prashant Varanasi 和 Simon Newton 編寫,旨在讓其他同事快速地熟悉和編寫 Go 程序。經過多年發展,現在的版本經過了多番修改和改進了。這是我們在 Uber 遵從的編程范式,但是很多都是可以通用的,如下是其他可以參考的鏈接:
所有的提交代碼都應該通過 golint
和 go vet
檢測,建議在代碼編輯器上面做如下設置:
- 保存的時候運行
goimports
- 使用
golint
和go vet
去做錯誤檢測。
你可以通過下面鏈接發現更多的 Go 編輯器的插件: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
2. 編程指南
2.1 指向 Interface 的指針
在我們日常使用中,基本上不會需要使用指向 interface 的指針。當我們將 interface 作為值傳遞的時候,底層數據就是指針。Interface 包括兩方面:
- 一個包含 type 信息的指針
- 一個指向數據的指針
如果你想要修改底層的數據,那么你只能使用 pointer。
2.2 Receiver 和 Interface
使用值作為 receiver 的時候 method 可以通過指針調用,也可以通過值來調用。
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"}}
// You can only call Read using a value
sVals[1].Read()
// This will not compile:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")
相似的,pointer 也可以滿足 interface 的要求,盡管 method 使用 value 作為 receiver。
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
// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
// i = s2Val
Effective Go 關于如何使用指針和值也有一些不錯的 practice: Pointers vs. Values.
2.3 mutex 默認 0 值是合法的
sync.Mutex
和 sync.RWMutex
的 0 值也是合法的,所以我們基本不需要聲明一個指針指向 mutex。
Bad
mu := new(sync.Mutex)
mu.Lock()
Good
var mu sync.Mutex
mu.Lock()
如果 struct 內部使用 mutex,在我們使用 struct 的指針類型時候,mutex 也可以是一個非指針類型的 field,或者直接嵌套在 struct 中。
Mutex 直接嵌套在 struct 中。
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]
}
將 Mutex 作為一個 struct 內部一個非指針類型 Field 使用。
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]
}
2.4 拷貝 Slice 和 Map
Slice 和 Map 都包含了對底層存儲數據的指針,所以注意在修改 slice 或者 map 數據的場景下,是不是使用了引用。
slice 和 map 作為參數
當把 slice 和 map 作為參數的時候,如果我們對 slice 或者 map 的做了引用操作,那么修改會修改掉原始值。如果這種修改不是預期的,那么要先進行 copy。
Bad
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// Did you mean to modify d1.trips?
trips[0] = ...
Good
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 作為返回值
當我們的函數返回 slice 或者 map 的時候,也要注意是不是直接返回了內部數據的引用到外部。
Bad
type Stats struct {
sync.Mutex
counters map[string]int
}
// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock()
return s.counters
}
// snapshot is no longer protected by the lock!
snapshot := stats.Snapshot()
Good
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 is now a copy.
snapshot := stats.Snapshot()
2.5 使用 defer 做資源清理
建議使用 defer 去做資源清理工作,比如文件,鎖等。
Bad
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// easy to miss unlocks due to multiple returns
Good
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// more readable
盡管使用 defer 會導致一定的性能開銷,但是大部分情況下這個開銷在你的整個鏈路上所占的比重往往是微乎其微,除非說真的是有非常高的性能需求。另外使用 defer 帶來的代碼可讀性的改進以及減少代碼發生錯誤的概率都是值得的。
2.6 channel 的 size 最好是 1 或者是 unbuffered
在使用 channel 的時候,最好將 size 設置為 1 或者使用 unbuffered channel。其他 size 的 channel 往往都會引入更多的復雜度,需要更多考慮上下游的設計。
Bad
// Ought to be enough for anybody!
c := make(chan int, 64)
Good
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)
2.7 枚舉變量應該從 1 開始
在 Go 語言中枚舉值的聲明典型方式是通過 const
和 iota
來聲明。由于 0 是默認值,所以枚舉值最好從一個非 0 值開始,比如 1。
Bad
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
Good
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
有一種例外情況:0 值是預期的默認行為的時候,枚舉值可以從 0 開始。
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
2.8 Error 類型
在 Go 語言中聲明 error 可以有多種方式:
-
errors.New
聲明包含簡單靜態字符串的 error -
fmt.Errorf
格式化 error string - 其他自定義類型使用了
Error()
方法 - 使用
"pkg/errors".Wrap
當要把 error 作為返回值的時候,可以考慮如下的處理方式
- 是不是不需要額外信息,如果是,
errors.New
就足夠了。 - client 需要檢測和處理返回的 error 嗎?如果是,最好使用實現了
Error()
方法的自定義類型,這樣可以包含更多的信息。 - error 是不是從下游函數傳遞過來的?如果是,考慮一下 error wrap,參考:section on error wrapping.
- 其他情況,
fmt.Errorf
一般足夠了。
對于 client 需要檢測和處理 error 的情況,這里詳細說一下。如果你要通過 errors.New
聲明一個簡單的 error,那么可以使用一個變量聲明:var ErrCouldNotOpen = errors.New("Could not open")
Bad
// 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")
}
}
}
Good
// 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 中包含更多的信息,而不僅僅類型原生 error 的這種簡單字符串,那么最好使用一個自定義類型。
Bad
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")
}
}
}
Good
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 類型的時候,最好 export 配套的檢測自定義 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")
}
}
2.9 Error Wrapping
在函數調用失敗的時候,有三種方式可以將下游的 error 傳遞出去:
- 直接返回失敗函數返回的 error。
- 使用
"pkg/errors".Wrap
增加更多的上下文信息,這種情況下可以使用"pkg/errors".Cause
去提取原始的 error 信息。 - 如果調用者不需要檢測和處理返回的 error 信息的話,可以直接使用
fmt.Errorf
將需要附加的信息進行格式化添加進去。
如果條件允許,最好增加上下文信息。比如 "connection refused" 和 "call service foo: connection refused" 這兩種錯誤信息在可讀性上比較也是高下立判。當增加上下文信息的時候,盡量保持簡潔。比如像 "failed to" 這種極其明顯的信息就沒有必要寫上去了。
Bad
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %s", err)
}
Good
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %s", err)
}
另外對于需要傳播到其他系統的 error,也要有明顯的標識信息,比如在 log 的最前面增加 err
等字樣。
更多參考:Don't just check errors, handle them gracefully.
2.10 類型轉換失敗處理
類型轉換失敗會導致進程 panic,所以對于類型轉換,一定要使用 "comma ok" 的范式來處理。
Bad
t := i.(string)
Good
t, ok := i.(string)
if !ok {
// handle the error gracefully
}
2.11 不要 panic
對于線上環境要盡量避免 panic。在很多情況下,panic 都是引起雪崩效應的罪魁禍首。一旦 error 發生,我們應該向上游調用者返回 error,并且容許調用者對 error 進行檢測和處理。
Bad
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])
}
Good
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。
在跑 test case 的時候,使用 t.Fatal
或者 t.FailNow
,而不是 panic 來保證這個 test case 會被標記為失敗的。
Bad
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
}
Good
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
}
2.12 使用 go.uber.org/atomic
這個是 Uber 內部對原生包 sync/atomic
的一種封裝,隱藏了底層數據類型。
Bad
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!
}
Good
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()
}
3. 性能相關
3.1 類型轉換時,使用 strconv 替換 fmt
當基本類型和 string 互轉的時候,strconv
要比 fmt
快。
Bad
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
}
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
Good
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
}
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
3.2 避免 string to byte 的不必要頻繁轉換
在通過 string 創建 byte slice 的時候,不要在循環語句中重復的轉換,而是要將重復的轉換邏輯提到循環外面,做一次即可。(看上去很 general 的建議)
Bad
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
BenchmarkBad-4 50000000 22.2 ns/op
Good
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
BenchmarkGood-4 500000000 3.25 ns/op
4. 編程風格
4.1 聲明語句分組
import 語句分組
Bad
import "a"
import "b"
Good
import (
"a"
"b"
)
常量、變量以及 type 聲明
Bad
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
Good
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
import 根據導入的包進行順序分組。(其他庫我們其實可以再細分 private 庫和 public 庫)
- 標準庫
- 其他庫
Bad
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
Good
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
4.2 package 命名
package 命名的幾條規則:
- 全小寫。不包含大寫字母或者下劃線。
- 簡潔。
- 不要使用復數。比如,使用
net/url
,而不是net/urls
。 - 避免:"common", "util", "shared", "lib",不解釋。
更多參考:
4.3 函數命名
函數命名遵從社區規范: MixedCaps for function names 。有一種特例是 TestCase 中為了方便測試做的函數命名,比如:TestMyFunction_WhatIsBeingTested
。
4.4 import 別名
當 package 的名字和 import 的 path 的最后一個元素不同的時候,必須要起別名。
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
另外,import 別名要盡量避免,只要在不得不起別名的時候再這么做,比如避免沖突。
Bad
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
Good
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
4.5 函數分組和排序
- 函數應該按調用順序排序
- 一個文件中的函數應該按 receiver 排序
newXYZ/NewXYZ
最好緊接著類型聲明后面,并在其他的 receiver 函數前面。
Bad
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{}
}
Good
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 {...}
4.6 避免代碼塊嵌套
優先處理異常情況,快速返回,避免代碼塊過多嵌套。看下面代碼會比較直觀。
Bad
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)
}
}
Good
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()
}
4.7 避免不必要的 else 語句
很多情況下,if - else 語句都能通過一個 if 語句表達,比如如下代碼。
Bad
var a int
if b {
a = 100
} else {
a = 10
}
Good
a := 10
if b {
a = 100
}
4.8 兩級 (two-level) 變量聲明
所有兩級變量聲明就是一個聲明的右值來自另一個表達式,這個時候第一級變量聲明就不需要指明類型,除非這兩個地方的數據類型不同。看代碼會更直觀一點。
Bad
var _s string = F()
func F() string { return "A" }
Good
var _s = F()
// Since F already states that it returns a string, we don't need to specify
// the type again.
func F() string { return "A" }
上面說的第二種兩邊數據類型不同的情況。
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.
4.9 對于不做 export 的全局變量使用前綴 _
對于同一個 package 下面的多個文件,一個文件中的全局變量可能會被其他文件誤用,所以建議使用 _ 來做前綴。(其實這條規則有待商榷)
Bad
// 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.
}
Good
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
4.10 struct 嵌套
struct 中的嵌套類型在 field 列表排在最前面,并且用空行分隔開。
Bad
type Client struct {
version int
http.Client
}
Good
type Client struct {
http.Client
version int
}
4.11 struct 初始化的時候帶上 Field
這樣會更清晰,也是 go vet 鼓勵的方式
Bad
k := User{"John", "Doe", true}
Good
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
4.12 局部變量聲明
變量聲明的時候可以使用 :=
以表示這個變量被顯示的設置為某個值。
Bad
var s = "foo"
Good
s := "foo"
但是對于某些情況使用 var 反而表示的更清晰,比如聲明一個空的 slice: Declaring Empty Slices
Bad
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
Good
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
4.13 nil 是合法的 slice
在返回值是 slice 類型的時候,直接返回 nil 即可,不需要顯式地返回長度為 0 的 slice。
Bad
if x == "" {
return []int{}
}
Good
if x == "" {
return nil
}
判斷 slice 是不是空的時候,使用 len(s) == 0
。
Bad
func isEmpty(s []string) bool {
return s == nil
}
Good
func isEmpty(s []string) bool {
return len(s) == 0
}
使用 var 聲明的 slice 空值可以直接使用,不需要 make()
。
Bad
nums := []int{}
// or, nums := make([]int)
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
Good
var nums []int
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
4.14 避免 scope
Bad
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
Good
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
當然某些情況下,scope 是不可避免的,比如
Bad
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
}
Good
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
4.15 避免參數語義不明確(Avoid Naked Parameters)
Naked Parameter 指的應該是意義不明確的參數,這種情況會破壞代碼的可讀性,可以使用 C 分格的注釋(/*...*/
)進行注釋。
Bad
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
Good
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
對于上面的示例代碼,還有一種更好的處理方式是將上面的 bool 類型換成自定義類型。
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)
4.16 使用原生字符串,避免轉義
Go 支持使用反引號,也就是 "`" 來表示原生字符串,在需要轉義的場景下,我們應該盡量使用這種方案來替換。
Bad
wantError := "unknown name:\"test\""
Good
wantError := `unknown error:"test"`
4.17 Struct 引用初始化
使用 &T{}
而不是 new(T)
來聲明對 T 類型的引用,使用 &T{}
的方式我們可以和 struct 聲明方式 T{}
保持統一。
Bad
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
Good
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
4.18 字符串 string format
如果我們要在 Printf 外面聲明 format 字符串的話,使用 const,而不是變量,這樣 go vet 可以對 format 字符串做靜態分析。
Bad
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
Good
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
4.19 Printf 風格函數命名
當聲明 Printf
風格的函數時,確保 go vet
可以對其進行檢測。可以參考:Printf family 。
另外也可以在函數名字的結尾使用 f 結尾,比如: WrapF
,而不是 Wrap
。然后使用 go vet
$ go vet -printfuncs=wrapf,statusf
更多參考: go vet: Printf family check.
5. 編程模式(Patterns)
5.1 Test Tables
當測試邏輯是重復的時候,通過 subtests 使用 table 驅動的方式編寫 case 代碼看上去會更簡潔。
Bad
// 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)
Good
// 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,都會顯得更加的清晰。
在命名方面,我們將 struct 的 slice 命名為 tests
,同時每一個 test case 命名為 tt
。而且,我們強烈建議通過 give
和 want
前綴來表示 test case 的 input 和 output 的值。
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
5.2 Functional Options
關于 functional options 簡單來說就是通過類似閉包的方式來進行函數傳參。
Bad
// 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 */)
Good
// 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 */)
更多參考:
注:關于 functional option 這種方式我本人也強烈推薦,我很久以前也寫過一篇類似的文章,感興趣的可以移步: 寫擴展性好的代碼:函數
6. 總結
Uber 開源的這個文檔,通篇讀下來給我印象最深的就是:保持代碼簡潔,并具有良好可讀性。不得不說,相比于國內很多 “代碼能跑就完事了” 這種寫代碼的態度,這篇文章或許可以給我們更多的啟示和參考。