Uber正式開源Go語言編程規範,內部已使用多年

近日,Uber開源了其公司內部使用的《Go 語言編程規範》。該指南是爲了使代碼庫更易於管理,同時讓工程師有效地使用Go語言特性。文檔中詳細描述了在 Uber 編寫 Go 代碼的各種注意事項,包括具體的“Dos and Don’ts of writing Go code at Uber”,也就是Go代碼應該怎樣寫、不該怎樣寫。今天我們就來簡單瞭解一下國外大廠都是如何來寫代碼的。行文倉促,錯誤之處,多多指正。

1. 介紹

英文原文標題是 Uber Go Style Guide,這裏的 Style 是指在管理我們代碼的時候可以遵從的一些約定。

這篇編程指南的初衷是更好的管理我們的代碼,包括去編寫什麼樣的代碼,以及不要編寫什麼樣的代碼。我們希望通過這份編程指南,代碼可以具有更好的維護性,同時能夠讓我們的開發同學更高效地編寫 Go 語言代碼。

這份編程指南最初由 Prashant VaranasiSimon Newton 編寫,旨在讓其他同事快速地熟悉和編寫 Go 程序。經過多年發展,現在的版本經過了多番修改和改進了。這是我們在 Uber 遵從的編程範式,但是很多都是可以通用的,如下是其他可以參考的鏈接:

所有的提交代碼都應該通過 golintgo vet 檢測,建議在代碼編輯器上面做如下設置:

  • 保存的時候運行 goimports
  • 使用 golintgo 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.Mutexsync.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 語言中枚舉值的聲明典型方式是通過 constiota 來聲明。由於 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。而且,我們強烈建議通過 givewant 前綴來表示 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

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),
)

更多參考:

注:關於 functional option 這種方式我本人也強烈推薦,我很久以前也寫過一篇類似的文章,感興趣的可以移步: 寫擴展性好的代碼:函數

6. 總結

Uber 開源的這個文檔,通篇讀下來給我印象最深的就是:保持代碼簡潔,並具有良好可讀性。不得不說,相比於國內很多 “代碼能跑就完事了” 這種寫代碼的態度,這篇文章或許可以給我們更多的啓示和參考。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章