Uber Go 語(yǔ)言編碼規(guī)范

Uber Go 語(yǔ)言編碼規(guī)范

Uber 是一家美國(guó)硅谷的科技公司力穗,也是 Go 語(yǔ)言的早期 adopter拙寡。其開(kāi)源了很多 golang 項(xiàng)目劳澄,諸如被 Gopher 圈熟知的 zap童本、jaeger 等摄职。2018 年年末 Uber 將內(nèi)部的 Go 風(fēng)格規(guī)范 開(kāi)源到 GitHub,經(jīng)過(guò)一年的積累和更新笼踩,該規(guī)范已經(jīng)初具規(guī)模逗爹,并受到廣大 Gopher 的關(guān)注。本文是該規(guī)范的中文版本嚎于。本版本會(huì)根據(jù)原版實(shí)時(shí)更新掘而。

版本

  • 當(dāng)前更新版本:2019-11-13 版本地址:commit:#71
  • 如果您發(fā)現(xiàn)任何更新、問(wèn)題或改進(jìn)于购,請(qǐng)隨時(shí) fork 和 PR
  • Please feel free to fork and PR if you find any updates, issues or improvement.

目錄

介紹

樣式 (style) 是支配我們代碼的慣例斑胜。術(shù)語(yǔ)樣式有點(diǎn)用詞不當(dāng)控淡,因?yàn)檫@些約定涵蓋的范圍不限于由 gofmt 替我們處理的源文件格式。

本指南的目的是通過(guò)詳細(xì)描述在 Uber 編寫(xiě) Go 代碼的注意事項(xiàng)來(lái)管理這種復(fù)雜性止潘。這些規(guī)則的存在是為了使代碼庫(kù)易于管理掺炭,同時(shí)仍然允許工程師更有效地使用 Go 語(yǔ)言功能。

該指南最初由 Prashant VaranasiSimon Newton 編寫(xiě)凭戴,目的是使一些同事能快速使用 Go涧狮。多年來(lái),該指南已根據(jù)其他人的反饋進(jìn)行了修改么夫。

本文檔記錄了我們?cè)?Uber 遵循的 Go 代碼中的慣用約定者冤。其中許多是 Go 的通用準(zhǔn)則,而其他擴(kuò)展準(zhǔn)則依賴于下面外部的指南:

  1. Effective Go
  2. The Go common mistakes guide

所有代碼都應(yīng)該通過(guò)golintgo vet的檢查并無(wú)錯(cuò)誤档痪。我們建議您將編輯器設(shè)置為:

  • 保存時(shí)運(yùn)行 goimports
  • 運(yùn)行 golintgo vet 檢查錯(cuò)誤

您可以在以下 Go 編輯器工具支持頁(yè)面中找到更為詳細(xì)的信息:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指導(dǎo)原則

指向 interface 的指針

您幾乎不需要指向接口類(lèi)型的指針涉枫。您應(yīng)該將接口作為值進(jìn)行傳遞,在這樣的傳遞過(guò)程中钞它,實(shí)質(zhì)上傳遞的底層數(shù)據(jù)仍然可以是指針拜银。

接口實(shí)質(zhì)上在底層用兩個(gè)字段表示:

  1. 一個(gè)指向某些特定類(lèi)型信息的指針殊鞭。您可以將其視為"type"遭垛。
  2. 數(shù)據(jù)指針。如果存儲(chǔ)的數(shù)據(jù)是指針操灿,則直接存儲(chǔ)锯仪。如果存儲(chǔ)的數(shù)據(jù)是一個(gè)值,則存儲(chǔ)指向該值的指針趾盐。

如果希望接口方法修改基礎(chǔ)數(shù)據(jù)庶喜,則必須使用指針傳遞。

接收器 (receiver) 與接口

使用值接收器的方法既可以通過(guò)值調(diào)用救鲤,也可以通過(guò)指針調(diào)用久窟。

例如,

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

// 你只能通過(guò)值調(diào)用 Read
sVals[1].Read()

// 這不能編譯通過(guò):
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// 通過(guò)指針既可以調(diào)用 Read本缠,也可以調(diào)用 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")

同樣斥扛,即使該方法具有值接收器,也可以通過(guò)指針來(lái)滿足接口丹锹。

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

//  下面代碼無(wú)法通過(guò)編譯稀颁。因?yàn)?s2Val 是一個(gè)值,而 S2 的 f 方法中沒(méi)有使用值接收器
//   i = s2Val

Effective Go 中有一段關(guān)于 pointers vs. values 的精彩講解楣黍。

零值 Mutex 是有效的

零值 sync.Mutexsync.RWMutex 是有效的匾灶。所以指向 mutex 的指針基本是不必要的。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

var mu sync.Mutex
mu.Lock()

</td></tr>
</tbody></table>

如果你使用結(jié)構(gòu)體指針租漂,mutex 可以非指針形式作為結(jié)構(gòu)體的組成字段阶女,或者更好的方式是直接嵌入到結(jié)構(gòu)體中颊糜。
如果是私有結(jié)構(gòu)體類(lèi)型或是要實(shí)現(xiàn) Mutex 接口的類(lèi)型,我們可以使用嵌入 mutex 的方法:

<table>
<tbody>
<tr><td>

type smap struct {
  sync.Mutex // only for unexported types(僅適用于非導(dǎo)出類(lèi)型)

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

</td><td>

type SMap struct {
  mu sync.Mutex // 對(duì)于導(dǎo)出類(lèi)型秃踩,請(qǐng)使用私有鎖

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

</td></tr>

</tr>
<tr>
<td>為私有類(lèi)型或需要實(shí)現(xiàn)互斥接口的類(lèi)型嵌入芭析。</td>
<td>對(duì)于導(dǎo)出的類(lèi)型,請(qǐng)使用專(zhuān)用字段吞瞪。</td>
</tr>

</tbody></table>

在邊界處拷貝 Slices 和 Maps

slices 和 maps 包含了指向底層數(shù)據(jù)的指針馁启,因此在需要復(fù)制它們時(shí)要特別注意。

接收 Slices 和 Maps

請(qǐng)記住芍秆,當(dāng) map 或 slice 作為函數(shù)參數(shù)傳入時(shí)惯疙,如果您存儲(chǔ)了對(duì)它們的引用,則用戶可以對(duì)其進(jìn)行修改妖啥。

<table>
<thead><tr><th>Bad</th> <th>Good</th></tr></thead>
<tbody>
<tr>
<td>

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

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

// 你是要修改 d1.trips 嗎霉颠?
trips[0] = ...

</td>
<td>

func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

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

// 這里我們修改 trips[0],但不會(huì)影響到 d1.trips
trips[0] = ...

</td>
</tr>

</tbody>
</table>

返回 slices 或 maps

同樣荆虱,請(qǐng)注意用戶對(duì)暴露內(nèi)部狀態(tài)的 map 或 slice 的修改蒿偎。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

// Snapshot 返回當(dāng)前狀態(tài)。
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot 不再受互斥鎖保護(hù)
// 因此對(duì) snapshot 的任何訪問(wèn)都將受到數(shù)據(jù)競(jìng)爭(zhēng)的影響
// 影響 stats.counters
snapshot := stats.Snapshot()

</td><td>

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 現(xiàn)在是一個(gè)拷貝
snapshot := stats.Snapshot()

</td></tr>
</tbody></table>

使用 defer 釋放資源

使用 defer 釋放資源怀读,諸如文件和鎖诉位。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

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

return newCount

// 當(dāng)有多個(gè) return 分支時(shí),很容易遺忘 unlock

</td><td>

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// 更可讀

</td></tr>
</tbody></table>

Defer 的開(kāi)銷(xiāo)非常小菜枷,只有在您可以證明函數(shù)執(zhí)行時(shí)間處于納秒級(jí)的程度時(shí)苍糠,才應(yīng)避免這樣做。使用 defer 提升可讀性是值得的啤誊,因?yàn)槭褂盟鼈兊某杀疚⒉蛔愕涝啦t。尤其適用于那些不僅僅是簡(jiǎn)單內(nèi)存訪問(wèn)的較大的方法,在這些方法中其他計(jì)算的資源消耗遠(yuǎn)超過(guò) defer蚊锹。

Channel 的 size 要么是 1瞳筏,要么是無(wú)緩沖的

channel 通常 size 應(yīng)為 1 或是無(wú)緩沖的。默認(rèn)情況下牡昆,channel 是無(wú)緩沖的姚炕,其 size 為零。任何其他尺寸都必須經(jīng)過(guò)嚴(yán)格的審查迁杨∽晷模考慮如何確定大小,是什么阻止了 channel 在負(fù)載下被填滿并阻止寫(xiě)入铅协,以及發(fā)生這種情況時(shí)發(fā)生了什么捷沸。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

// 應(yīng)該足以滿足任何情況!
c := make(chan int, 64)

</td><td>

// 大泻贰:1
c := make(chan int, 1) // 或者
// 無(wú)緩沖 channel痒给,大小為 0
c := make(chan int)

</td></tr>
</tbody></table>

枚舉從 1 開(kāi)始

在 Go 中引入枚舉的標(biāo)準(zhǔn)方法是聲明一個(gè)自定義類(lèi)型和一個(gè)使用了 iota 的 const 組说墨。由于變量的默認(rèn)值為 0,因此通常應(yīng)以非零值開(kāi)頭枚舉苍柏。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

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

</td><td>

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

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

</td></tr>
</tbody></table>

在某些情況下尼斧,使用零值是有意義的(枚舉從零開(kāi)始),例如试吁,當(dāng)零值是理想的默認(rèn)行為時(shí)棺棵。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

錯(cuò)誤類(lèi)型

Go 中有多種聲明錯(cuò)誤(Error) 的選項(xiàng):

  • errors.New 對(duì)于簡(jiǎn)單靜態(tài)字符串的錯(cuò)誤
  • fmt.Errorf 用于格式化的錯(cuò)誤字符串
  • 實(shí)現(xiàn) Error() 方法的自定義類(lèi)型
  • "pkg/errors".Wrap 的 Wrapped errors

返回錯(cuò)誤時(shí),請(qǐng)考慮以下因素以確定最佳選擇:

  • 這是一個(gè)不需要額外信息的簡(jiǎn)單錯(cuò)誤嗎熄捍?如果是這樣烛恤,errors.New 足夠了。

  • 客戶需要檢測(cè)并處理此錯(cuò)誤嗎余耽?如果是這樣缚柏,則應(yīng)使用自定義類(lèi)型并實(shí)現(xiàn)該 Error() 方法。

  • 您是否正在傳播下游函數(shù)返回的錯(cuò)誤碟贾?如果是這樣币喧,請(qǐng)查看本文后面有關(guān)錯(cuò)誤包裝 section on error wrapping 部分的內(nèi)容。

  • 否則 fmt.Errorf 就可以了袱耽。

如果客戶端需要檢測(cè)錯(cuò)誤杀餐,并且您已使用創(chuàng)建了一個(gè)簡(jiǎn)單的錯(cuò)誤 errors.New,請(qǐng)使用一個(gè)錯(cuò)誤變量扛邑。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

如果您有可能需要客戶端檢測(cè)的錯(cuò)誤怜浅,并且想向其中添加更多信息(例如,它不是靜態(tài)字符串)蔬崩,則應(yīng)使用自定義類(lèi)型。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

直接導(dǎo)出自定義錯(cuò)誤類(lèi)型時(shí)要小心搀暑,因?yàn)樗鼈円殉蔀槌绦虬?API 的一部分沥阳。最好公開(kāi)匹配器功能以檢查錯(cuò)誤。

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

錯(cuò)誤包裝 (Error Wrapping)

一個(gè)(函數(shù)/方法)調(diào)用失敗時(shí)自点,有三種主要的錯(cuò)誤傳播方式:

  • 如果沒(méi)有要添加的其他上下文桐罕,并且您想要維護(hù)原始錯(cuò)誤類(lèi)型,則返回原始錯(cuò)誤桂敛。

  • 添加上下文功炮,使用 "pkg/errors".Wrap 以便錯(cuò)誤消息提供更多上下文 ,"pkg/errors".Cause 可用于提取原始錯(cuò)誤。
    Use fmt.Errorf if the callers do not need to detect or handle that specific error case.

  • 如果調(diào)用者不需要檢測(cè)或處理的特定錯(cuò)誤情況术唬,使用 fmt.Errorf薪伏。

建議在可能的地方添加上下文,以使您獲得諸如“調(diào)用服務(wù) foo:連接被拒絕”之類(lèi)的更有用的錯(cuò)誤塘淑,而不是諸如“連接被拒絕”之類(lèi)的模糊錯(cuò)誤萝招。

在將上下文添加到返回的錯(cuò)誤時(shí),請(qǐng)避免使用“failed to”之類(lèi)的短語(yǔ)來(lái)保持上下文簡(jiǎn)潔存捺,這些短語(yǔ)會(huì)陳述明顯的內(nèi)容槐沼,并隨著錯(cuò)誤在堆棧中的滲透而逐漸堆積:

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

<tr><td>

failed to x: failed to y: failed to create new store: the error

</td><td>

x: y: new store: the error

</td></tr>
</tbody></table>

但是,一旦將錯(cuò)誤發(fā)送到另一個(gè)系統(tǒng)捌治,就應(yīng)該明確消息是錯(cuò)誤消息(例如使用err標(biāo)記具滴,或在日志中以”Failed”為前綴)凹嘲。

另請(qǐng)參見(jiàn) Don't just check errors, handle them gracefully. 不要只是檢查錯(cuò)誤,要優(yōu)雅地處理錯(cuò)誤

處理類(lèi)型斷言失敗

type assertion 的單個(gè)返回值形式針對(duì)不正確的類(lèi)型將產(chǎn)生 panic构韵。因此凶朗,請(qǐng)始終使用“comma ok”的慣用法。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

t := i.(string)

</td><td>

t, ok := i.(string)
if !ok {
  // 優(yōu)雅地處理錯(cuò)誤
}

</td></tr>
</tbody></table>

不要 panic

在生產(chǎn)環(huán)境中運(yùn)行的代碼必須避免出現(xiàn) panic显拳。panic 是 cascading failures 級(jí)聯(lián)失敗的主要根源 棚愤。如果發(fā)生錯(cuò)誤,該函數(shù)必須返回錯(cuò)誤杂数,并允許調(diào)用方?jīng)Q定如何處理它宛畦。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

panic/recover 不是錯(cuò)誤處理策略。僅當(dāng)發(fā)生不可恢復(fù)的事情(例如:nil 引用)時(shí)揍移,程序才必須 panic次和。程序初始化是一個(gè)例外:程序啟動(dòng)時(shí)應(yīng)使程序中止的不良情況可能會(huì)引起 panic。

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

即使在測(cè)試代碼中那伐,也優(yōu)先使用t.Fatal或者t.FailNow而不是 panic 來(lái)確保失敗被標(biāo)記踏施。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}

</td><td>

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}

</td></tr>
</tbody></table>

使用 go.uber.org/atomic

使用 sync/atomic 包的原子操作對(duì)原始類(lèi)型 (int32, int64等)進(jìn)行操作,因?yàn)楹苋菀淄浭褂迷硬僮鱽?lái)讀取或修改變量罕邀。

go.uber.org/atomic 通過(guò)隱藏基礎(chǔ)類(lèi)型為這些操作增加了類(lèi)型安全性畅形。此外,它包括一個(gè)方便的atomic.Bool類(lèi)型诉探。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

性能

性能方面的特定準(zhǔn)則只適用于高頻場(chǎng)景日熬。

優(yōu)先使用 strconv 而不是 fmt

將原語(yǔ)轉(zhuǎn)換為字符串或從字符串轉(zhuǎn)換時(shí),strconv速度比fmt快阵具。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}

</td><td>

for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}

</td></tr>
<tr><td>

BenchmarkFmtSprint-4    143 ns/op    2 allocs/op

</td><td>

BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

</td></tr>
</tbody></table>

避免字符串到字節(jié)的轉(zhuǎn)換

不要反復(fù)從固定字符串創(chuàng)建字節(jié) slice碍遍。相反定铜,請(qǐng)執(zhí)行一次轉(zhuǎn)換并捕獲結(jié)果。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}

</td><td>

data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}

</tr>
<tr><td>

BenchmarkBad-4   50000000   22.2 ns/op

</td><td>

BenchmarkGood-4  500000000   3.25 ns/op

</td></tr>
</tbody></table>

盡量初始化時(shí)指定 Map 容量

在盡可能的情況下怕敬,在使用 make() 初始化的時(shí)候提供容量信息

make(map[T1]T2, hint)

make() 提供容量信息(hint)嘗試在初始化時(shí)調(diào)整 map 大小揣炕,
這減少了在將元素添加到 map 時(shí)增長(zhǎng)和分配的開(kāi)銷(xiāo)。
注意东跪,map 不能保證分配 hint 個(gè)容量畸陡。因此,即使提供了容量虽填,添加元素仍然可以進(jìn)行分配丁恭。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

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

</td><td>


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

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

</td></tr>
<tr><td>

m 是在沒(méi)有大小提示的情況下創(chuàng)建的; 在運(yùn)行時(shí)可能會(huì)有更多分配斋日。

</td><td>

m 是有大小提示創(chuàng)建的牲览;在運(yùn)行時(shí)可能會(huì)有更少的分配。

</td></tr>
</tbody></table>

規(guī)范

一致性

本文中概述的一些標(biāo)準(zhǔn)都是客觀性的評(píng)估恶守,是根據(jù)場(chǎng)景第献、上下文、或者主觀性的判斷兔港;

但是最重要的是庸毫,保持一致.

一致性的代碼更容易維護(hù)、是更合理的衫樊、需要更少的學(xué)習(xí)成本飒赃、并且隨著新的約定出現(xiàn)或者出現(xiàn)錯(cuò)誤后更容易遷移、更新科侈、修復(fù) bug

相反载佳,一個(gè)單一的代碼庫(kù)會(huì)導(dǎo)致維護(hù)成本開(kāi)銷(xiāo)、不確定性和認(rèn)知偏差兑徘。所有這些都會(huì)直接導(dǎo)致速度降低刚盈、
代碼審查痛苦、而且增加 bug 數(shù)量

將這些標(biāo)準(zhǔn)應(yīng)用于代碼庫(kù)時(shí)挂脑,建議在 package(或更大)級(jí)別進(jìn)行更改,子包級(jí)別的應(yīng)用程序通過(guò)將多個(gè)樣式引入到同一代碼中窥妇,違反了上述關(guān)注點(diǎn)旺垒。

相似的聲明放在一組

Go 語(yǔ)言支持將相似的聲明放在一個(gè)組內(nèi)茬斧。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

import "a"
import "b"

</td><td>

import (
  "a"
  "b"
)

</td></tr>
</tbody></table>

這同樣適用于常量、變量和類(lèi)型聲明:

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>


const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

</td><td>

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

</td></tr>
</tbody></table>

僅將相關(guān)的聲明放在一組刁俭。不要將不相關(guān)的聲明放在一組。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

type Operation int

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

</td><td>

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"

</td></tr>
</tbody></table>

分組使用的位置沒(méi)有限制韧涨,例如:你可以在函數(shù)內(nèi)部使用它們:

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

  ...
}

</td><td>

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

  ...
}

</td></tr>
</tbody></table>

import 分組

導(dǎo)入應(yīng)該分為兩組:

  • 標(biāo)準(zhǔn)庫(kù)
  • 其他庫(kù)

默認(rèn)情況下牍戚,這是 goimports 應(yīng)用的分組侮繁。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

</td><td>

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

</td></tr>
</tbody></table>

包名

當(dāng)命名包時(shí),請(qǐng)按下面規(guī)則選擇一個(gè)名稱:

  • 全部小寫(xiě)如孝。沒(méi)有大寫(xiě)或下劃線宪哩。
  • 大多數(shù)使用命名導(dǎo)入的情況下,不需要重命名第晰。
  • 簡(jiǎn)短而簡(jiǎn)潔锁孟。請(qǐng)記住,在每個(gè)使用的地方都完整標(biāo)識(shí)了該名稱茁瘦。
  • 不用復(fù)數(shù)品抽。例如net/url,而不是net/urls甜熔。
  • 不要用“common”圆恤,“util”,“shared”或“l(fā)ib”腔稀。這些是不好的盆昙,信息量不足的名稱。

另請(qǐng)參閱 Package NamesGo 包樣式指南.

函數(shù)名

我們遵循 Go 社區(qū)關(guān)于使用 MixedCaps 作為函數(shù)名 的約定烧颖。有一個(gè)例外弱左,為了對(duì)相關(guān)的測(cè)試用例進(jìn)行分組,函數(shù)名可能包含下劃線炕淮,如:TestMyFunction_WhatIsBeingTested.

導(dǎo)入別名

如果程序包名稱與導(dǎo)入路徑的最后一個(gè)元素不匹配拆火,則必須使用導(dǎo)入別名。

import (
  "net/http"

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

在所有其他情況下涂圆,除非導(dǎo)入之間有直接沖突们镜,否則應(yīng)避免導(dǎo)入別名。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

</td><td>

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

  nettrace "golang.net/x/trace"
)

</td></tr>
</tbody></table>

函數(shù)分組與順序

  • 函數(shù)應(yīng)按粗略的調(diào)用順序排序润歉。
  • 同一文件中的函數(shù)應(yīng)按接收者分組模狭。

因此,導(dǎo)出的函數(shù)應(yīng)先出現(xiàn)在文件中踩衩,放在struct, const, var定義的后面嚼鹉。

在定義類(lèi)型之后,但在接收者的其余方法之前驱富,可能會(huì)出現(xiàn)一個(gè) newXYZ()/NewXYZ()

由于函數(shù)是按接收者分組的锚赤,因此普通工具函數(shù)應(yīng)在文件末尾出現(xiàn)。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

減少嵌套

代碼應(yīng)通過(guò)盡可能先處理錯(cuò)誤情況/特殊情況并盡早返回或繼續(xù)循環(huán)來(lái)減少嵌套褐鸥。減少嵌套多個(gè)級(jí)別的代碼的代碼量线脚。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

不必要的 else

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

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

a := 10
if b {
  a = 100
}

</td></tr>
</tbody></table>

頂層變量聲明

在頂層浑侥,使用標(biāo)準(zhǔn)var關(guān)鍵字姊舵。請(qǐng)勿指定類(lèi)型,除非它與表達(dá)式的類(lèi)型不同寓落。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

var _s string = F()

func F() string { return "A" }

</td><td>

var _s = F()
// 由于 F 已經(jīng)明確了返回一個(gè)字符串類(lèi)型括丁,因此我們沒(méi)有必要顯式指定_s 的類(lèi)型
// 還是那種類(lèi)型

func F() string { return "A" }

</td></tr>
</tbody></table>

如果表達(dá)式的類(lèi)型與所需的類(lèi)型不完全匹配,請(qǐng)指定類(lèi)型零如。

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// F 返回一個(gè) myError 類(lèi)型的實(shí)例躏将,但是我們要 error 類(lèi)型

對(duì)于未導(dǎo)出的頂層常量和變量,使用_作為前綴

在未導(dǎo)出的頂級(jí)varsconsts考蕾, 前面加上前綴_祸憋,以使它們?cè)谑褂脮r(shí)明確表示它們是全局符號(hào)。

例外:未導(dǎo)出的錯(cuò)誤值肖卧,應(yīng)以err開(kāi)頭蚯窥。

基本依據(jù):頂級(jí)變量和常量具有包范圍作用域。使用通用名稱可能很容易在其他文件中意外使用錯(cuò)誤的值塞帐。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

</td></tr>
</tbody></table>

結(jié)構(gòu)體中的嵌入

嵌入式類(lèi)型(例如 mutex)應(yīng)位于結(jié)構(gòu)體內(nèi)的字段列表的頂部拦赠,并且必須有一個(gè)空行將嵌入式字段與常規(guī)字段分隔開(kāi)。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

type Client struct {
  version int
  http.Client
}

</td><td>

type Client struct {
  http.Client

  version int
}

</td></tr>
</tbody></table>

使用字段名初始化結(jié)構(gòu)體

初始化結(jié)構(gòu)體時(shí)葵姥,幾乎始終應(yīng)該指定字段名稱『墒螅現(xiàn)在由 go vet 強(qiáng)制執(zhí)行。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

例外:如果有 3 個(gè)或更少的字段榔幸,則可以在測(cè)試表中省略字段名稱允乐。

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

本地變量聲明

如果將變量明確設(shè)置為某個(gè)值,則應(yīng)使用短變量聲明形式 (:=)削咆。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

var s = "foo"

</td><td>

s := "foo"

</td></tr>
</tbody></table>

但是牍疏,在某些情況下,var 使用關(guān)鍵字時(shí)默認(rèn)值會(huì)更清晰拨齐。例如鳞陨,聲明空切片。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

func f(list []int) {
  filtered := []int{}
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

</td><td>

func f(list []int) {
  var filtered []int
  for _, v := range list {
    if v > 10 {
      filtered = append(filtered, v)
    }
  }
}

</td></tr>
</tbody></table>

nil 是一個(gè)有效的 slice

nil 是一個(gè)有效的長(zhǎng)度為 0 的 slice瞻惋,這意味著厦滤,

  • 您不應(yīng)明確返回長(zhǎng)度為零的切片。應(yīng)該返回nil 來(lái)代替歼狼。

    <table>
    <thead><tr><th>Bad</th><th>Good</th></tr></thead>
    <tbody>
    <tr><td>

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

    </td><td>

    if x == "" {
      return nil
    }
    

    </td></tr>
    </tbody></table>

  • 要檢查切片是否為空馁害,請(qǐng)始終使用len(s) == 0。而非 nil蹂匹。

    <table>
    <thead><tr><th>Bad</th><th>Good</th></tr></thead>
    <tbody>
    <tr><td>

    func isEmpty(s []string) bool {
      return s == nil
    }
    

    </td><td>

    func isEmpty(s []string) bool {
      return len(s) == 0
    }
    

    </td></tr>
    </tbody></table>

  • 零值切片(用var聲明的切片)可立即使用,無(wú)需調(diào)用make()創(chuàng)建凹蜈。

    <table>
    <thead><tr><th>Bad</th><th>Good</th></tr></thead>
    <tbody>
    <tr><td>

    nums := []int{}
    // or, nums := make([]int)
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }
    

    </td><td>

    var nums []int
    
    if add1 {
      nums = append(nums, 1)
    }
    
    if add2 {
      nums = append(nums, 2)
    }
    

    </td></tr>
    </tbody></table>

小變量作用域

如果有可能限寞,盡量縮小變量作用范圍忍啸。除非它與 減少嵌套的規(guī)則沖突。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

如果需要在 if 之外使用函數(shù)調(diào)用的結(jié)果履植,則不應(yīng)嘗試縮小范圍计雌。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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
}

</td><td>

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

if err := cfg.Decode(data); err != nil {
  return err
}

fmt.Println(cfg)
return nil

</td></tr>
</tbody></table>

避免參數(shù)語(yǔ)義不明確(Avoid Naked Parameters)

函數(shù)調(diào)用中的意義不明確的參數(shù)可能會(huì)損害可讀性。當(dāng)參數(shù)名稱的含義不明顯時(shí)玫霎,請(qǐng)為參數(shù)添加 C 樣式注釋 (/* ... */)

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

</td><td>

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

</td></tr>
</tbody></table>

對(duì)于上面的示例代碼凿滤,還有一種更好的處理方式是將上面的 bool 類(lèi)型換成自定義類(lèi)型。將來(lái)庶近,該參數(shù)可以支持不僅僅局限于兩個(gè)狀態(tài)(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)

使用原始字符串字面值,避免轉(zhuǎn)義

Go 支持使用 原始字符串字面值鼻种,也就是 " ` " 來(lái)表示原生字符串反番,在需要轉(zhuǎn)義的場(chǎng)景下,我們應(yīng)該盡量使用這種方案來(lái)替換叉钥。

可以跨越多行并包含引號(hào)罢缸。使用這些字符串可以避免更難閱讀的手工轉(zhuǎn)義的字符串。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

wantError := `unknown error:"test"`

</td></tr>
</tbody></table>

初始化 Struct 引用

在初始化結(jié)構(gòu)引用時(shí)投队,請(qǐng)使用&T{}代替new(T)枫疆,以使其與結(jié)構(gòu)體初始化一致。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

sval := T{Name: "foo"}

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

</td><td>

sval := T{Name: "foo"}

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

</td></tr>
</tbody></table>

初始化 Maps

對(duì)于空 map 請(qǐng)使用 make(..) 初始化敷鸦, 并且 map 是通過(guò)編程方式填充的息楔。
這使得 map 初始化在表現(xiàn)上不同于聲明,并且它還可以方便地在 make 后添加大小提示轧膘。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

var (
  // m1 讀寫(xiě)安全;
  // m2 在寫(xiě)入時(shí)會(huì) panic
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

</td><td>

var (
  // m1 讀寫(xiě)安全;
  // m2 在寫(xiě)入時(shí)會(huì) panic
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

</td></tr>
<tr><td>

聲明和初始化看起來(lái)非常相似的钞螟。

</td><td>

聲明和初始化看起來(lái)差別非常大。

</td></tr>
</tbody></table>

在盡可能的情況下谎碍,請(qǐng)?jiān)诔跏蓟瘯r(shí)提供 map 容量大小鳞滨,詳細(xì)請(qǐng)看 盡量初始化時(shí)指定 Map 容量

另外蟆淀,如果 map 包含固定的元素列表拯啦,則使用 map literals(map 初始化列表) 初始化映射。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

基本準(zhǔn)則是:在初始化時(shí)使用 map 初始化列表 來(lái)添加一組固定的元素熔任。否則使用 make (如果可以褒链,請(qǐng)盡量指定 map 容量)。

字符串 string format

如果你為Printf-style 函數(shù)聲明格式字符串疑苔,請(qǐng)將格式化字符串放在外面甫匹,并將其設(shè)置為const常量。

這有助于go vet對(duì)格式字符串執(zhí)行靜態(tài)分析。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

</td><td>

const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

</td></tr>
</tbody></table>

命名 Printf 樣式的函數(shù)

聲明Printf-style 函數(shù)時(shí)兵迅,請(qǐng)確保go vet可以檢測(cè)到它并檢查格式字符串抢韭。

這意味著您應(yīng)盡可能使用預(yù)定義的Printf-style 函數(shù)名稱。go vet將默認(rèn)檢查這些恍箭。有關(guān)更多信息刻恭,請(qǐng)參見(jiàn) Printf 系列

如果不能使用預(yù)定義的名稱扯夭,請(qǐng)以 f 結(jié)束選擇的名稱:Wrapf鳍贾,而不是Wrapgo vet可以要求檢查特定的 Printf 樣式名稱交洗,但名稱必須以f結(jié)尾骑科。

$ go vet -printfuncs=wrapf,statusf

另請(qǐng)參閱 go vet: Printf family check.

編程模式

表驅(qū)動(dòng)測(cè)試

當(dāng)測(cè)試邏輯是重復(fù)的時(shí)候,通過(guò) subtests 使用 table 驅(qū)動(dòng)的方式編寫(xiě) case 代碼看上去會(huì)更簡(jiǎn)潔藕筋。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

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

</td><td>

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

</td></tr>
</tbody></table>

很明顯纵散,使用 test table 的方式在代碼邏輯擴(kuò)展的時(shí)候,比如新增 test case隐圾,都會(huì)顯得更加的清晰伍掀。

我們遵循這樣的約定:將結(jié)構(gòu)體切片稱為tests。 每個(gè)測(cè)試用例稱為tt暇藏。此外蜜笤,我們鼓勵(lì)使用givewant前綴說(shuō)明每個(gè)測(cè)試用例的輸入和輸出值。

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

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

功能選項(xiàng)

功能選項(xiàng)是一種模式盐碱,您可以在其中聲明一個(gè)不透明 Option 類(lèi)型把兔,該類(lèi)型在某些內(nèi)部結(jié)構(gòu)中記錄信息。您接受這些選項(xiàng)的可變編號(hào)瓮顽,并根據(jù)內(nèi)部結(jié)構(gòu)上的選項(xiàng)記錄的全部信息采取行動(dòng)县好。

將此模式用于您需要擴(kuò)展的構(gòu)造函數(shù)和其他公共 API 中的可選參數(shù),尤其是在這些功能上已經(jīng)具有三個(gè)或更多參數(shù)的情況下暖混。

<table>
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
<tbody>
<tr><td>

// 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 */)

</td><td>

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

</td></tr>
</tbody></table>

還可以參考下面資料:

本文由zshipu.com學(xué)習(xí)筆記或整理或轉(zhuǎn)載缕贡,如有侵權(quán)請(qǐng)聯(lián)系,必改之拣播。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末晾咪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子贮配,更是在濱河造成了極大的恐慌谍倦,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泪勒,死亡現(xiàn)場(chǎng)離奇詭異昼蛀,居然都是意外死亡宴猾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)曹洽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鳍置,“玉大人,你說(shuō)我怎么就攤上這事送淆。” “怎么了怕轿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵偷崩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我撞羽,道長(zhǎng)阐斜,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任诀紊,我火速辦了婚禮谒出,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘邻奠。我一直安慰自己笤喳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布碌宴。 她就那樣靜靜地躺著杀狡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贰镣。 梳的紋絲不亂的頭發(fā)上呜象,一...
    開(kāi)封第一講書(shū)人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音碑隆,去河邊找鬼恭陡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛上煤,可吹牛的內(nèi)容都是我干的休玩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼楼入,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼哥捕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起嘉熊,我...
    開(kāi)封第一講書(shū)人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤遥赚,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后阐肤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體凫佛,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡讲坎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了愧薛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晨炕。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖毫炉,靈堂內(nèi)的尸體忽然破棺而出瓮栗,到底是詐尸還是另有隱情,我是刑警寧澤瞄勾,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布费奸,位于F島的核電站,受9級(jí)特大地震影響进陡,放射性物質(zhì)發(fā)生泄漏愿阐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一趾疚、第九天 我趴在偏房一處隱蔽的房頂上張望缨历。 院中可真熱鬧,春花似錦糙麦、人聲如沸辛孵。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)觉吭。三九已至,卻和暖如春仆邓,著一層夾襖步出監(jiān)牢的瞬間鲜滩,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工节值, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留徙硅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓搞疗,卻偏偏與公主長(zhǎng)得像嗓蘑,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子匿乃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容