uber go 編碼規范

內容列表

指導原則

指向interface的指針

幾乎不需要指向接口類型的指針。我們應該將接口進行值傳遞,在這樣的傳遞過程中,實質上傳遞的底層數據仍然可以是指針。

接口實質上在底層用兩個字段表示

  1. 一個包含type信息的指針。
  2. 數據指針。如果存儲的數據是指針,則直接存儲。如果存儲的數據是一個值,則存儲指向該值的指針。

如果要接口方法修改底層數據,則必須用指向目標對象的指針賦值給接口類型變量

接收器與接口

使用值接收器的方法既可以通過值調用,也可以通過指針調用。

例如,

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.

零值Mutex是有效的

sync.Mutex和sync.RWMutex是有效的。因此你幾乎不需要一個指向mutex的指針。

Bad

mu := new(sync.Mutex)
mu.Lock()

Good

var mu sync.Mutex
mu.Lock()

如果你使用結構體指針,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]
}

如果是私有結構體類型或是要實現Mutex接口的類型,我們可以使用嵌入mutex的方法

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

slices和maps包含了指向底層數據的指針,因此在需要復制它們時要特別注意。

接收Slices和Maps

Go語言中所有的傳參都是值傳遞(傳值),都是一個副本,一個拷貝。因為拷貝的內容有時候是非引用類型(int、string、struct等這些),這樣就在函數中就無法修改原內容數據;有的是引用類型(指針、map、slice、chan等這些),這樣就可以修改原內容數據。

需要特別注意!當map或slice作為函數參數傳入時,如果你不小心保留了對它們的引用,則用戶可以對其進行修改。

Bad

func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

trips := ...
d1.SetTrips(trips)

// 是要修改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)

// 修改trips[0],但不會影響到d1.trips
trips[0] = ...

slice 和 map 作為返回值

當我們的函數返回 slice 或者 map 的時候,也要注意是不是直接返回了內部數據的引用到外部。

Bad

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的訪問將會受到數據競爭的影響
snapshot := stats.Snapshot()

Good

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做清理

使用defer清理資源,諸如文件和鎖。

Bad

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// 當有多個return分支時,很容易遺忘unlock

Good

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應為1或是無緩沖的。默認情況下,channel是無緩沖的,其size為零。任何其他size都必須經過嚴格的審查。使用channel時應該慎重考慮如何確定大小,需要思考:是什么阻止了通道在負載下被填滿,阻止寫入,以及發生這種情況時發生了什么。

Bad

// 對任何人來說都應該夠了!
c := make(chan int, 64)

Good

// channel尺寸:1
c := make(chan int, 1) // 或者
// 無緩沖channel,大小為0
c := make(chan int)

枚舉從1開始

在Go中引入枚舉的標準方法是聲明一個自定義類型和一個使用了iotaconst組。由于變量的默認值為0,因此通常應以非零值開頭枚舉。

Bad

type Operation int

const (
  Add Operation = iota
  Subtrac
  Multiply
)

// Add=0, Subtract=1, Multiply=2

Good

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 就足夠了。

  • 客戶需要檢測并處理此錯誤嗎?如果是這樣,則應使用自定義類型并實現該Error()方法。

  • 您是否正在傳播下游函數返回的錯誤?如果是這樣,請查看本文后面有關錯誤包裝部分的內容

  • 否則, fmt.Errorf 足夠.

如果客戶需要檢測錯誤,并且是通過errors.New創建的一個簡單的錯誤,請使用var 聲明這個錯誤類型。

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")
  }
}

如果您有可能需要客戶端檢測的錯誤,并且想向其中添加更多信息(例如,它不是靜態字符串),則應使用自定義類型。

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")
    }
  }
}

直接導出自定義錯誤類型時要小心,因為這意味著他們已經成為包的公開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可用于提取原始錯誤。
  • 使用 fmt.Errorf 。如果調用者不需要檢測或處理的特定錯誤情況。

\color{#FF0000}{Attention!}
建議在可能的地方添加上下文,以使您獲得諸如“調用服務foo:連接被拒絕”之類的更有用的錯誤,而不是諸如“連接被拒絕”之類的模糊錯誤。

在將上下文添加到返回的錯誤時,請避免使用"failed to"之類的短語來保持上下文簡潔,這些短語會陳述明顯的內容,并隨著錯誤在堆棧中的滲透而逐漸堆積:

Bad

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %s", err)
}
failed to x: failed to y: failed to create new store: the error

Good

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %s", err)
}
x: y: new store: the error

但是,一旦將錯誤發送到另一個系統,就應該明確消息是錯誤消息(例如使用err標記,或在日志中以”Failed”為前綴)。

參見 Don't just check errors, handle them gracefully.

處理類型斷言失敗

類型斷言的單個返回值形式針對不正確的類型將產生panic。因此,請始終使用“comma ok”的慣用方法。

Bad

t := i.(string)

Good

t, ok := i.(string)
if !ok {
  // 優雅處理錯誤
}

不要Panic

\color{#FF0000}{Attention!}
在生產環境中運行的代碼必須避免出現panic。panic是級聯故障的主要根源 。如果發生錯誤,該函數必須返回錯誤,并允許調用方決定如何處理它。

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不是錯誤處理策略。僅當發生不可恢復的事情(例如:nil引用)時,程序才必須panic。程序初始化是一個例外:程序啟動時應使程序中止的不良情況可能會引起panic。

var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))

即便是在test中,也優先使用t.Fatalt.FailNow 來標記test是失敗的,而不是panic。

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")
}

避免可變全局變量

避免改變全局變量,而選擇依賴注入。
這適用于函數指針以及其他類型的值。

Bad

// sign.go

var _timeNow = time.Now

func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}
// sign_test.go

func TestSign(t *testing.T) {
  oldTimeNow := _timeNow
  _timeNow = func() time.Time {
    return someFixedTime
  }
  defer func() { _timeNow = oldTimeNow }()

  assert.Equal(t, want, sign(give))
}

Good

// sign.go

type signer struct {
  now func() time.Time
}

func newSigner() *signer {
  return &signer{
    now: time.Now,
  }
}

func (s *signer) Sign(msg string) string {
  now := s.now()
  return signWithTime(msg, now)
}
// sign_test.go

func TestSigner(t *testing.T) {
  s := newSigner()
  s.now = func() time.Time {
    return someFixedTime
  }

  assert.Equal(t, want, s.Sign(give))
}

性能

性能方面的特定準則,僅適用于熱路徑。

strconv性能優于fmt

將原語轉換為字符串或從字符串轉換時,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

避免string到byte的轉換

\color{#FF0000}{Attention!}
不要重復從固定字符串創建字節片。相反,請執行一次轉換并捕獲結果。

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

最好指定Map容量大小

如果可以,在用 make()初始化map時給定Map容量大小暗示。

make(map[T1]T2, hint)

make() 添加一定的容量提示一定程度上會減少map底層添加元素時的內存不斷分配的消耗。

Bad

m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}

m is created without a size hint; there may be more allocations at assignment time.

Good


files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

m is created with a size hint; there may be fewer allocations at assignment time.

代碼風格

保持一致

本文件中概述的一些準則可以客觀評估;
其他的是情景的、上下文的或主觀的。

最重要的是, 保持一致.

一致的代碼更容易維護,更容易合理化,需要的更少認知開銷,并且隨著新的約定的出現更容易遷移、更新或者修復同一類錯誤。

相反,在一個單一的代碼庫會出現多種不同或沖突的風格的代碼風格,會導致維護開銷、不確定性和認知失調,所有這些都會直接導致開發速度地下、代碼審查異常痛苦,當然還有BUG。

將這些準則應用于代碼庫時,建議更改在包級別(或更高的)層級生成.

相似的聲明放在一組

Go語言支持將相似的聲明放在一個組內:

Bad

import "a"
import "b"

Good

import (
  "a"
  "b"
)

這同樣適用于常量、變量和類型聲明:

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
)

僅將相關的聲明放在一組。不要將不相關的聲明放在一組。

Bad

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  ENV_VAR = "MY_ENV"
)

Good

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"

分組使用的位置沒有限制,例如:你可以在函數內部使用它們:

Bad

func f() string {
  var red = color.New(0xff0000)
  var green = color.New(0x00ff00)
  var blue = color.New(0x0000ff)

  ...
}

Good

func f() string {
  var (
    red   = color.New(0xff0000)
    green = color.New(0x00ff00)
    blue  = color.New(0x0000ff)
  )

  ...
}

Import組內的包導入順序

應該有兩類導入組:

  • 標準庫
  • 其他

默認情況下,這是goimports應用的分組。

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

包名

當命名包時,請按下面規則選擇一個名稱:

  • 全部小寫。沒有大寫或下劃線。
  • 大多數使用命名導入的情況下,不需要重命名。
  • 簡短而簡潔。請記住,在每個使用的地方都完整標識了該名稱。
  • 不用復數。例如 net/url, 而不是 net/urls.
  • 別寫“common”,“util”,“shared”或“lib”。這些是不好的,信息量不足的名稱。

另請參閱[包名規范] 和 [Go包樣式指南].

函數名

我們遵循Go社區關于使用MixedCaps作為函數名約定。有一個例外,為了對相關的測試用例進行分組,函數名可能包含下劃線,如: TestMyFunction_WhatIsBeingTested

導入別名

如果程序包名稱與導入路徑的最后一個元素不匹配,則必須使用導入別名。

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

在所有其他情況下,除非導入之間有直接沖突,否則應避免導入別名。

Bad

import (
  "fmt"
  "os"


  nettrace "golang.net/x/trace"
)

Good

import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

函數分組與順序

  • 函數應按粗略的調用順序排序。
  • 同一文件中的函數應按接收者分組。

因此,導出的函數應先出現在文件中,放在struct, const, var定義的后面。

在定義類型之后,但在接收者的其余方法之前,可能會出現一個newXYZ()/NewXYZ()

由于函數是按接收者分組的,因此普通工具函數應在文件末尾出現。

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 {...}

減少嵌套

代碼應通過盡可能先處理錯誤情況/特殊情況,盡早返回或繼續循環來減少嵌套。減少嵌套多個級別的代碼的代碼量。

看下面的示例,優先判斷錯誤,有錯誤盡快continue進行循環。正常的無需判斷err的邏輯放在最后。

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()
}

不必要的else

如果在if的兩個分支中都設置了變量,則可以將其替換為單個if。

Bad

var a int
if b {
  a = 100
} else {
  a = 10
}

Good

a := 10
if b {
  a = 100
}

頂層變量聲明

在頂層,使用標準var關鍵字。請勿指定類型,除非它與表達式的類型不同。

Bad

var _s string = F()

func F() string { return "A" }

Good

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 returns an object of type myError but we want error.

結構體中的嵌入

嵌入式類型(例如mutex)應位于結構體內的字段列表的頂部,并且必須有一個空行將嵌入式字段與常規字段分隔開。

Bad

type Client struct {
  version int
  http.Client
}

Good

type Client struct {
  http.Client

  version int
}

使用字段名初始化結構體

初始化結構體時,幾乎始終應該指定字段名稱。現在由go vet強制執行。

Bad

k := User{"John", "Doe", true}

Good

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

例外:如果有3個或更少的字段,則可以在測試表中省略字段名稱。

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

本地變量聲明

如果將變量明確設置為某個值,則應使用短變量聲明形式(:=)。

Bad

var s = "foo"

Good

s := "foo"

但是,在某些情況下,var使用關鍵字時默認值會更清晰。例如,聲明空切片

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

nil是一個有效的slice

nil是一個有效的長度為0的slice,這意味著:

  • 您不應明確返回長度為零的切片。返回nil來代替。

Bad

if x == "" {
  return []int{}
}

Good

if x == "" {
  return nil
}
  • 要檢查切片是否為空,請始終使用len(s) == 0。不要檢查 nil

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

縮小變量作用域

如果有可能,盡量縮小變量作用范圍。除非它與減少嵌套的規則沖突。

Bad

err := ioutil.WriteFile(name, data, 0644)
if err != nil {
 return err
}

Good

if err := ioutil.WriteFile(name, data, 0644); err != nil {
 return err
}

如果需要在if之外使用函數調用的結果,則不應嘗試縮小范圍。

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

避免裸參數

函數調用中的裸參數可能會損害可讀性。當參數名稱的含義不明顯時,請為參數添加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類型替換為自定義類型,以獲得更易讀和類型安全的代碼。將來,該參數不僅允許兩個狀態(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

wantError := "unknown name:\"test\""

Good

wantError := `unknown error:"test"`

初始化結構體引用

在初始化結構引用時,請使用&T{}代替new(T),以使其與結構體初始化一致。

Bad

sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"

Good

sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

初始化Maps

make(..) 創建空的Maps,用程序填充Maps。
這中寫法使得map初始化和聲明看起來是不同的,如果需要,以后可以很容易地為map添加大小提示。

Bad

var (
  // m1 is safe to read and write;
  // m2 will panic on writes.
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

聲明和初始化看起來是相似的。
Good

var (
  // m1 is safe to read and write;
  // m2 will panic on writes.
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

聲明和初始化看起來是不同的。

在可能的情況下,在使用make()初始化時給定容量提示。具體內容見:最好指定Map容量大小

另一方面,如果map包含固定的元素列表,使用map文本初始化map。

Bad

m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3

Good

m := map[T1]T2{
  k1: v1,
  k2: v2,
  k3: v3,
}

基本的經驗法則是在添加一組固定的元素,否則使用make(并指定大小提示,如果有的話)。

格式化字符串放在Printf外部

如果為Printf-style 函數聲明格式化字符串,將格式化字符串放在函數外面 ,并將其設置為const常量。

這有助于 go vet 對格式字符串進行靜態分析。

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)

為Printf樣式函數命名

聲明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.

模式

測試表

在核心測試邏輯重復時,將表驅動測試與子測試一起使用,以避免重復代碼。

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

測試表在錯誤消息處理上,包括注入上下文信息、減少重復的邏輯、添加新的測試用例,都變得更加容易。

我們遵循這樣的約定:將結構體切片稱為tests。 每個測試用例稱為tt。此外,我們鼓勵使用 givewant前綴說明每個測試用例的輸入和輸出值。

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

功能選項

功能選項是一種模式,聲明一個不透明Option類型,該類型記錄某些內部結構體的信息。您的函數接受這些不定數量的選項參數,并將選項參數上的信息作用于內部結構上。

此模式可用于擴展構造函數和實現其他公共 API 中的可選參數,特別是這些參數已經有三個或者超過三個的情況下。

Bad

// package db

func Open(
  addr string,
  cache bool,
  logger *zap.Logger
) (*Connection, error) {
  // ...
}

必須始終提供cache和logger參數,即使用戶希望使用默認值。

db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log)

Good

// package db

type Option interface {
  // ...
}

func WithCache(c bool) Option {
  // ...
}

func WithLogger(log *zap.Logger) Option {
  // ...
}

// Open creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  // ...
}

Options 僅在需要時提供

db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
  addr,
  db.WithCache(false),
  db.WithLogger(log),
)

我們建議的實現此模式的方法是使用Option接口,

保存不可導出的方法,在不可導出的Option上記錄選項結構。

type options struct {
  cache  bool
  logger *zap.Logger
}

type Option interface {
  apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
  opts.cache = bool(c)
}

func WithCache(c bool) Option {
  return cacheOption(c)
}

type loggerOption struct {
  Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
  opts.Logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
  return loggerOption{Log: log}
}

// Open creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    cache:  defaultCache,
    logger: zap.NewNop(),
  }

  for _, o := range opts {
    o.apply(&options)
  }

  // ...
}

注意,有一種用閉包實現這個模式的方法,但是我們相信上面的模式為使用者提供了更大的靈活性,便于用戶調試和測試。特別是,它允許在測試和模擬中相互比較,這用閉包是不可能的。此外,它還允許選項實現其他接口,包括fmt.Stringer允許用"用戶可讀的字符串"表示option。

另見,

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容

  • 相信很多人前兩天都看到 Uber 在 github 上面開源的 Go 語言編程規范了,原文在這里:https://...
    legendtkl閱讀 4,825評論 0 5
  • 更合理的方式寫 JavaScript 原文看 這里 ,收錄在此為便于查閱。 類型 1.1 基本類型:直接存取。字符...
    殺破狼real閱讀 8,828評論 0 6
  • 第5章 引用類型(返回首頁) 本章內容 使用對象 創建并操作數組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,261評論 0 4
  • 1.安裝 https://studygolang.com/dl 2.使用vscode編輯器安裝go插件 3.go語...
    go含羞草閱讀 1,565評論 0 6
  • ??引用類型的值(對象)是引用類型的一個實例。 ??在 ECMAscript 中,引用類型是一種數據結構,用于將數...
    霜天曉閱讀 1,078評論 0 1