uber go 編碼規(guī)范

內(nèi)容列表

指導(dǎo)原則

指向interface的指針

幾乎不需要指向接口類型的指針。我們應(yīng)該將接口進(jìn)行值傳遞,在這樣的傳遞過程中,實(shí)質(zhì)上傳遞的底層數(shù)據(jù)仍然可以是指針。

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

  1. 一個(gè)包含type信息的指針。
  2. 數(shù)據(jù)指針。如果存儲(chǔ)的數(shù)據(jù)是指針,則直接存儲(chǔ)。如果存儲(chǔ)的數(shù)據(jù)是一個(gè)值,則存儲(chǔ)指向該值的指針。

如果要接口方法修改底層數(shù)據(jù),則必須用指向目標(biāo)對(duì)象的指針賦值給接口類型變量

接收器與接口

使用值接收器的方法既可以通過值調(diào)用,也可以通過指針調(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"}}

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

//下面無法通過編譯:
//sVals[1].Write("test")

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

//通過指針既可以調(diào)用Read,也可以調(diào)用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

//下面代碼無法通過編譯。因?yàn)閟2Val是一個(gè)值,而S2的f方法中沒有使用值接收器
//i = s2Val

Effective Go 有詳盡的解釋 Pointers vs. Values.

零值Mutex是有效的

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

Bad

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

Good

var mu sync.Mutex
mu.Lock()

如果你使用結(jié)構(gòu)體指針,mutex可以非指針形式作為結(jié)構(gòu)體的組成字段,或者更好的方式是直接嵌入到結(jié)構(gòu)體中。

type smap struct {
  sync.Mutex // 僅針對(duì)非導(dǎo)出類型

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

如果是私有結(jié)構(gòu)體類型或是要實(shí)現(xiàn)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]
}

對(duì)于導(dǎo)出類型,請(qǐng)使用私有鎖

在邊界處拷貝Slices和Maps

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

接收Slices和Maps

Go語言中所有的傳參都是值傳遞(傳值),都是一個(gè)副本,一個(gè)拷貝。因?yàn)榭截惖膬?nèi)容有時(shí)候是非引用類型(int、string、struct等這些),這樣就在函數(shù)中就無法修改原內(nèi)容數(shù)據(jù);有的是引用類型(指針、map、slice、chan等這些),這樣就可以修改原內(nèi)容數(shù)據(jù)。

需要特別注意!當(dāng)map或slice作為函數(shù)參數(shù)傳入時(shí),如果你不小心保留了對(duì)它們的引用,則用戶可以對(duì)其進(jìn)行修改。

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],但不會(huì)影響到d1.trips
trips[0] = ...

slice 和 map 作為返回值

當(dāng)我們的函數(shù)返回 slice 或者 map 的時(shí)候,也要注意是不是直接返回了內(nèi)部數(shù)據(jù)的引用到外部。

Bad

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的訪問將會(huì)受到數(shù)據(jù)競(jìng)爭(zhēng)的影響
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現(xiàn)在是一個(gè)拷貝
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

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

Good

p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// 可讀性更高

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

Channel的size要么是1要么是無緩沖的

channel通常size應(yīng)為1或是無緩沖的。默認(rèn)情況下,channel是無緩沖的,其size為零。任何其他size都必須經(jīng)過嚴(yán)格的審查。使用channel時(shí)應(yīng)該慎重考慮如何確定大小,需要思考:是什么阻止了通道在負(fù)載下被填滿,阻止寫入,以及發(fā)生這種情況時(shí)發(fā)生了什么。

Bad

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

Good

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

枚舉從1開始

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

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

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

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

錯(cuò)誤類型

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

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

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

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

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

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

  • 否則, fmt.Errorf 足夠.

如果客戶需要檢測(cè)錯(cuò)誤,并且是通過errors.New創(chuàng)建的一個(gè)簡(jiǎn)單的錯(cuò)誤,請(qǐng)使用var 聲明這個(gè)錯(cuò)誤類型。

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

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

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

直接導(dǎo)出自定義錯(cuò)誤類型時(shí)要小心,因?yàn)檫@意味著他們已經(jīng)成為包的公開API的一部分了。更好的方式是暴露一個(gè)匹配函數(shù)來檢測(cè)錯(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ò)誤包裝

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

  • 如果沒有要添加的其他上下文,并且您想要維護(hù)原始錯(cuò)誤類型,則返回原始錯(cuò)誤。
  • 使用"pkg/errors".Wrap添加上下文,以便錯(cuò)誤消息可以提供更多上下文。 "pkg/errors".Cause可用于提取原始錯(cuò)誤。
  • 使用 fmt.Errorf 。如果調(diào)用者不需要檢測(cè)或處理的特定錯(cuò)誤情況。

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

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

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

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

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

處理類型斷言失敗

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

Bad

t := i.(string)

Good

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

不要Panic

\color{#FF0000}{Attention!}
在生產(chǎn)環(huán)境中運(yùn)行的代碼必須避免出現(xiàn)panic。panic是級(jí)聯(lián)故障的主要根源 。如果發(fā)生錯(cuò)誤,該函數(shù)必須返回錯(cuò)誤,并允許調(diào)用方?jīng)Q定如何處理它。

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不是錯(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"))

即便是在test中,也優(yōu)先使用t.Fatalt.FailNow 來標(biāo)記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")
}

避免可變?nèi)肿兞?/h3>

避免改變?nèi)肿兞?,而選擇依賴注入。
這適用于函數(shù)指針以及其他類型的值。

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

性能

性能方面的特定準(zhǔn)則,僅適用于熱路徑。

strconv性能優(yōu)于fmt

將原語轉(zhuǎn)換為字符串或從字符串轉(zhuǎn)換時(shí),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的轉(zhuǎn)換

\color{#FF0000}{Attention!}
不要重復(fù)從固定字符串創(chuàng)建字節(jié)片。相反,請(qǐng)執(zhí)行一次轉(zhuǎn)換并捕獲結(jié)果。

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時(shí)給定Map容量大小暗示。

make(map[T1]T2, hint)

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

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.

代碼風(fēng)格

保持一致

本文件中概述的一些準(zhǔn)則可以客觀評(píng)估;
其他的是情景的、上下文的或主觀的。

最重要的是, 保持一致.

一致的代碼更容易維護(hù),更容易合理化,需要的更少認(rèn)知開銷,并且隨著新的約定的出現(xiàn)更容易遷移、更新或者修復(fù)同一類錯(cuò)誤。

相反,在一個(gè)單一的代碼庫(kù)會(huì)出現(xiàn)多種不同或沖突的風(fēng)格的代碼風(fēng)格,會(huì)導(dǎo)致維護(hù)開銷、不確定性和認(rèn)知失調(diào),所有這些都會(huì)直接導(dǎo)致開發(fā)速度地下、代碼審查異常痛苦,當(dāng)然還有BUG。

將這些準(zhǔn)則應(yīng)用于代碼庫(kù)時(shí),建議更改在包級(jí)別(或更高的)層級(jí)生成.

相似的聲明放在一組

Go語言支持將相似的聲明放在一個(gè)組內(nèi):

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
)

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

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"

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

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組內(nèi)的包導(dǎo)入順序

應(yīng)該有兩類導(dǎo)入組:

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

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

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

包名

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

  • 全部小寫。沒有大寫或下劃線。
  • 大多數(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)參閱[包名規(guī)范] 和 [Go包樣式指南].

函數(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)入別名。

Bad

import (
  "fmt"
  "os"


  nettrace "golang.net/x/trace"
)

Good

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

  nettrace "golang.net/x/trace"
)

函數(shù)分組與順序

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

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

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

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

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

減少嵌套

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

看下面的示例,優(yōu)先判斷錯(cuò)誤,有錯(cuò)誤盡快continue進(jìn)行循環(huán)。正常的無需判斷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的兩個(gè)分支中都設(shè)置了變量,則可以將其替換為單個(gè)if。

Bad

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

Good

a := 10
if b {
  a = 100
}

頂層變量聲明

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

Bad

var _s string = F()

func F() string { return "A" }

Good

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

func F() string { return "A" }

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

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.

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

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

Bad

type Client struct {
  version int
  http.Client
}

Good

type Client struct {
  http.Client

  version int
}

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

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

Bad

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

Good

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

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

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

本地變量聲明

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

Bad

var s = "foo"

Good

s := "foo"

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

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是一個(gè)有效的slice

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

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

Bad

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

Good

if x == "" {
  return nil
}
  • 要檢查切片是否為空,請(qǐng)始終使用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)可立即使用,無需調(diào)用make創(chuàng)建。

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

縮小變量作用域

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

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之外使用函數(shù)調(diào)用的結(jié)果,則不應(yīng)嘗試縮小范圍。

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

避免裸參數(shù)

函數(shù)調(diào)用中的裸參數(shù)可能會(huì)損害可讀性。當(dāng)參數(shù)名稱的含義不明顯時(shí),請(qǐng)為參數(shù)添加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類型替換為自定義類型,以獲得更易讀和類型安全的代碼。將來,該參數(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支持原始字符串字面值,可以跨越多行并包含引號(hào)。使用這些字符串可以避免更難閱讀的手工轉(zhuǎn)義的字符串。

Bad

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

Good

wantError := `unknown error:"test"`

初始化結(jié)構(gòu)體引用

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

Bad

sval := T{Name: "foo"}

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

Good

sval := T{Name: "foo"}

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

初始化Maps

make(..) 創(chuàng)建空的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()初始化時(shí)給定容量提示。具體內(nèi)容見:最好指定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,
}

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

格式化字符串放在Printf外部

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

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

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樣式函數(shù)命名

聲明Printf-style函數(shù)時(shí),請(qǐng)確保go vet可以檢查它的格式化字符串。

這意味著應(yīng)盡可能使用預(yù)定義的Printf-style函數(shù)名稱。go vet默認(rèn)會(huì)檢查它們。更多相關(guān)信息,請(qǐng)參見Printf系列。

如果不能使用預(yù)定義的名稱,請(qǐng)以 f 結(jié)尾:Wrapf,而非 Wrap。因?yàn)?go vet 可以指定檢查特定的 Printf 樣式名稱,但名稱必須以 f 結(jié)尾。

$ go vet -printfuncs=wrapf,statusf

另見 go vet: Printf family check.

模式

測(cè)試表

在核心測(cè)試邏輯重復(fù)時(shí),將表驅(qū)動(dòng)測(cè)試與子測(cè)試一起使用,以避免重復(fù)代碼。

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

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

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

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

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

功能選項(xiàng)

功能選項(xiàng)是一種模式,聲明一個(gè)不透明Option類型,該類型記錄某些內(nèi)部結(jié)構(gòu)體的信息。您的函數(shù)接受這些不定數(shù)量的選項(xiàng)參數(shù),并將選項(xiàng)參數(shù)上的信息作用于內(nèi)部結(jié)構(gòu)上。

此模式可用于擴(kuò)展構(gòu)造函數(shù)和實(shí)現(xiàn)其他公共 API 中的可選參數(shù),特別是這些參數(shù)已經(jīng)有三個(gè)或者超過三個(gè)的情況下。

Bad

// package db

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

必須始終提供cache和logger參數(shù),即使用戶希望使用默認(rèn)值。

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 僅在需要時(shí)提供

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

我們建議的實(shí)現(xiàn)此模式的方法是使用Option接口,

保存不可導(dǎo)出的方法,在不可導(dǎo)出的Option上記錄選項(xiàng)結(jié)構(gòu)。

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

  // ...
}

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

另見,

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容