相信很多人前兩天都看到 Uber 在 github 上面開源的 Go 語言編程規範了,原文在這裡:https://github.com/uber-go/guide/blob/master/style.md 。我們今天就來簡單了解一下國外大廠都是如何來寫代碼的。行文倉促,錯誤之處,多多指正。另外如果覺得還不錯,也歡迎分享給更多的人。
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: {&34;}}// You can only call Read using a valuesVals[1].Read()// This will not compile:// sVals[1].Write(&34;)sPtrs := map[int]*S{1: {&34;}}// You can call both Read and Write using a pointersPtrs[1].Read()sPtrs[1].Write(&34;)相似的,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 Fi = s1Vali = s1Ptri = s2Ptr// The following doesn&34;pkg/errors&34;Could not open&34;could not open&34;could not open&34;unknown error&34;could not open&34;unknown error&34;file %q not found&34;not found&34;unknown error&34;file %q not found&34;unknown error&34;file %q not found&34;foo&34;unknown error&34;pkg/errors&34;pkg/errors&34;failed to create new store: %s&34;new store: %s&34;bar must not be empty&34;USAGE: foo <bar>&34;bar must not be empty&34;USAGE: foo <bar>&34;&34;test&34;failed to set up test&34;&34;test&34;failed to set up test&34;Hello world&34;Hello world&34;a&34;b&34;a&34;b&34;fmt&34;os&34;go.uber.org/atomic&34;golang.org/x/sync/errgroup&34;fmt&34;os&34;go.uber.org/atomic&34;golang.org/x/sync/errgroup&34;net/http&34;example.com/client-go&34;example.com/trace/v2&34;fmt&34;os&34;golang.net/x/trace&34;fmt&34;os&34;runtime/trace&34;golang.net/x/trace&34;Invalid v: %v&34;Invalid v: %v&34;A&39;t need to specify// the type again.func F() string { return &34; }
上面說的第二種兩邊數據類型不同的情況。
type myError struct{}func (myError) Error() string { return &34; }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.goconst ( defaultPort = 8080 defaultUser = &34;)// bar.gofunc Bar() { defaultPort := 9090 ... fmt.Println(&34;, defaultPort) // We will not see a compile error if the first line of // Bar() is deleted.}
Good
// foo.goconst ( _defaultPort = 8080 _defaultUser = &34;)
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{&34;, &34;, true}
Good
k := User{ FirstName: &34;, LastName: &34;, Admin: true,}
4.12 局部變量聲明
變量聲明的時候可以使用 := 以表示這個變量被顯示的設置為某個值。
Bad
var s = &34;Goods := &34;
但是對於某些情況使用 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 == &34; { return []int{}}
Good
if x == &34; { 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 []intif 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(&34;, true, true)
Good
// func printInfo(name string, isLocal, done bool)printInfo(&34;, true /* isLocal */, true /* done */)
對於上面的示例代碼,還有一種更好的處理方式是將上面的 bool 類型換成自定義類型。
type Region intconst ( UnknownRegion Region = iota Local)type Status intconst ( 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 := &34;test\&34;
Good
wantError := `unknown error:&34;`
4.17 Struct 引用初始化
使用 &T{} 而不是 new(T) 來聲明對 T 類型的引用,使用 &T{} 的方式我們可以和 struct 聲明方式 T{} 保持統一。
Bad
sval := T{Name: &34;}// inconsistentsptr := new(T)sptr.Name = &34;
Good
sval := T{Name: &34;}sptr := &T{Name: &34;}
4.18 字符串 string format
如果我們要在 Printf 外面聲明 format 字符串的話,使用 const,而不是變量,這樣 go vet 可以對 format 字符串做靜態分析。
Bad
msg := &34;fmt.Printf(msg, 1, 2)
Good
const msg = &34;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(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)host, port, err = net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)host, port, err = net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)host, port, err = net.SplitHostPort(&34;)require.NoError(t, err)assert.Equal(t, &34;, host)assert.Equal(t, &34;, port)
Good
// func TestSplitHostPort(t *testing.T)tests := []struct{ give string wantHost string wantPort string}{ { give: &34;, wantHost: &34;, wantPort: &34;, }, { give: &34;, wantHost: &34;, wantPort: &34;, }, { give: &34;, wantHost: &34;, wantPort: &34;, }, { give: &34;, wantHost: &34;, wantPort: &34;, },}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 dbfunc 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 */)Goodtype 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),)
更多參考:
Self-referential functions and the design of optionsFunctional options for friendly APIs
註:關於 functional option 這種方式我本人也強烈推薦,我很久以前也寫過一篇類似的文章,感興趣的可以移步: 寫擴展性好的代碼:函數
6. 總結
Uber 開源的這個文檔,通篇讀下來給我印象最深的就是:保持代碼簡潔,並具有良好可讀性。不得不說,相比於國內很多 「代碼能跑就完事了」 這種寫代碼的態度,這篇文章或許可以給我們更多的啟示和參考。