App下載

Uber 的 Go 規(guī)范

W3cschool 2022-12-08 10:11:44 瀏覽數(shù) (2737)
反饋
原文: https://mp.weixin.qq.com/s/UQwOJVqm5gnQPMgSE3MW9w

Uber公司推出的Go語(yǔ)言規(guī)范,建議沒(méi)看過(guò)的同學(xué)看一遍,里面的規(guī)范很多,不見(jiàn)得每一條都采納,不現(xiàn)實(shí),選一些適合的可以落地執(zhí)行的拿來(lái)參考就行。

介紹

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

本指南最初是由Prashant Varanasi和Simon Newton創(chuàng)建的,是為了讓一些同事盡快掌握Go的使用方法。多年來(lái),我們根據(jù)其他人的反饋對(duì)它進(jìn)行了修改。

這記錄了我們?cè)?Uber 所遵循的 Go 代碼中的習(xí)慣性約定。其中很多是Go里面的一般準(zhǔn)則,而其他的則是根據(jù)外部資源進(jìn)行擴(kuò)展:

  1. Effective Go
  2. Go Common Mistakes
  3. Go Code Review Comments

我們的目標(biāo)是使代碼樣本準(zhǔn)確地適用于Go的兩個(gè)最新的次要版本。

所有代碼在通過(guò)golint和go vet運(yùn)行時(shí)應(yīng)該是沒(méi)有錯(cuò)誤的。我們建議將您的編輯器設(shè)置為:

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

你可以在這里找到編輯器支持Go工具的信息:https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指南

指向interface的指針

你幾乎不需要一個(gè)指向 interface 的指針,interface類(lèi)型數(shù)據(jù)應(yīng)該直接傳遞,但實(shí)際上 interface 底層是一個(gè)指針。

interface 類(lèi)型包括兩部分:

  1. 一個(gè)指向特定類(lèi)型的指針??梢詫⑵湟暈?"類(lèi)型"。
  2. 數(shù)據(jù)指針。如果底層數(shù)據(jù)是指針,會(huì)被直接存儲(chǔ)。如果底層數(shù)據(jù)是值,那會(huì)存儲(chǔ)這個(gè)數(shù)據(jù)的指針。

如果你想要接口方法修改基礎(chǔ)數(shù)據(jù),那必須使用指針。

驗(yàn)證接口合法性

在編譯期驗(yàn)證接口的合法性,需要驗(yàn)證的有:

  • 驗(yàn)證導(dǎo)出類(lèi)型在作為API時(shí)是否實(shí)現(xiàn)了特定接口
  • 實(shí)現(xiàn)一個(gè)接口的導(dǎo)出和非導(dǎo)出類(lèi)型是集合的一部分
  • 違反接口合理性無(wú)法編譯通過(guò),通知用戶
Bad Good
type Handler struct {
  // ...
}



func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}
type Handler struct {
  // ...
}

var _ http.Handler = (*Handler)(nil)

func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

如果*Handler沒(méi)有實(shí)現(xiàn)http.Handler接口,那么var _ http.Handler = (*Handler)(nil)語(yǔ)句在編譯期就會(huì)報(bào)錯(cuò);

賦值語(yǔ)句的右邊部門(mén)應(yīng)是斷言類(lèi)型的零值。對(duì)于指針類(lèi)型(像*Handler)、slice和map類(lèi)型,零值為nil,對(duì)于結(jié)構(gòu)體類(lèi)型,零值為空結(jié)構(gòu)體,下面是空結(jié)構(gòu)體的例子。

type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}

// LogHandler{}是空結(jié)構(gòu)體
var _ http.Handler = LogHandler{}

func (h LogHandler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

接收者和接口

使用 值類(lèi)型 接收者的方法既可以通過(guò)值調(diào)用,也可以通過(guò)指針調(diào)用。

使用 指針類(lèi)型 接收者的方法只能通過(guò)指針或者 addressable values調(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"}}

// 值類(lèi)型可以調(diào)用Read()
sVals[1].Read()

// 值類(lèi)型調(diào)用Write方法會(huì)報(bào)編譯錯(cuò)誤
//  sVals[1].Write("test")

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

// 指針類(lèi)型 Read 和 Write 方法都可以調(diào)用
sPtrs[1].Read()
sPtrs[1].Write("test")

同樣的,接口可以通過(guò)指針調(diào)用,即使這個(gè)方法的接收者是指類(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

// 這個(gè)例子編譯會(huì)報(bào)錯(cuò),因?yàn)閟2Val是值類(lèi)型,而S2的方法里接收者是指針類(lèi)型.
//   i = s2Val

Effective Go 有一段關(guān)于 Pointers vs. Values 的優(yōu)秀講解

Mutexes的零值是有效的

sync.Mutex 和 sync.RWMutex 的零值是有效的,所以不需要實(shí)例化一個(gè)Mutex的指針。

Bad Good
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()

如果結(jié)構(gòu)體中包含mutex,在使用結(jié)構(gòu)體的指針時(shí),mutex應(yīng)該是結(jié)構(gòu)體的非指針字段,也不要把mutex內(nèi)嵌到結(jié)構(gòu)體中,即使結(jié)構(gòu)體是非導(dǎo)出類(lèi)型。

Bad Good
type SMap struct {
  sync.Mutex

  data map[string]string
}

func NewSMap() *SMap {
  return &SMap{
    data: make(map[string]string),
  }
}

func (m *SMap) Get(k stringstring {
  m.Lock()
  defer m.Unlock()

  return m.data[k]
}
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 stringstring {
  m.mu.Lock()
  defer m.mu.Unlock()

  return m.data[k]
}

隱式嵌入Mutex, 其LockUnlock方法是SMap公開(kāi)API中不明確說(shuō)明的一部分。

mutex和它SMap方法的實(shí)現(xiàn)細(xì)節(jié)對(duì)調(diào)用方屏蔽。

在邊界拷貝Slices和Maps

slice 和 map 類(lèi)型包含指向data數(shù)據(jù)的指針,所以當(dāng)你需要復(fù)制時(shí)應(yīng)格外注意。

接收 Slices 和 Maps

如果在函數(shù)調(diào)用中傳遞 map 或 slice, 請(qǐng)記住這個(gè)函數(shù)可以修改它。

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

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

// 這樣賦值會(huì)影響到 d1.trips
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = make([]Trip, len(trips))
  copy(d.trips, trips)
}

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

// 這樣賦值不會(huì)影響 d1.trips(因?yàn)?nbsp;SetTrips 內(nèi)部有copy).
trips[0] = ...

返回 Slices 和 Maps

同樣,請(qǐng)注意用戶對(duì) maps 或 slices 的修改暴露了內(nèi)部狀態(tài)。

Bad Good
type Stats struct {
  mu sync.Mutex
  counters map[string]int
}

// Snapshot 返回當(dāng)前的 stats.
func (s *Stats) Snapshot() map[string]int {
  s.mu.Lock()
  defer s.mu.Unlock()

  return s.counters
}

// snapshot 變量不在受 mutex鎖保護(hù),任何對(duì) snapshot 的訪問(wèn)會(huì)受數(shù)據(jù)竟態(tài)的影響
snapshot := stats.Snapshot()
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]intlen(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// snapshot 現(xiàn)在只是個(gè)copy。
snapshot := stats.Snapshot()

使用Defer釋放資源

在讀寫(xiě)文件、使用鎖時(shí),使用 defer 釋放資源

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

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

return newCount

// 當(dāng)有多分支 return 時(shí),很容易漏寫(xiě)Unlock().
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// 使用defer在一個(gè)地方 Unlock, 代碼可讀性更好

調(diào)用 defer 的性能開(kāi)銷(xiāo)非常小,但如果你需要納秒級(jí)別的函數(shù)調(diào)用,那可能需要避免使用 defer。使用 defer 帶來(lái)的可讀性 勝過(guò)引入其它帶來(lái)的性能開(kāi)銷(xiāo)。defer 尤其適用于適用于那些不僅是內(nèi)存 放在的行數(shù)較多、邏輯較為復(fù)雜的大方法,這些方法中其他代碼邏輯的執(zhí)行成本比 defer 執(zhí)行成本更大。

Channel 大小應(yīng)為 0 或 1

Channels 的大小應(yīng)該是1或無(wú)緩沖的。默認(rèn)情況下,channels 是無(wú)緩沖的,size為0。其他size需經(jīng)過(guò)嚴(yán)格的審查。考慮 channel 的size 是如何定義的,是什么造成了 channel 在負(fù)荷情況下被寫(xiě)滿而無(wú)法寫(xiě)入,以及無(wú)法寫(xiě)入會(huì)發(fā)生什么。

Bad Good
// 這個(gè) size 對(duì)任何操作都?jí)蛄耍?/span>
c := make(chan int, 64)
// Size 是1
c := make(chan int, 1// or
// 無(wú)緩沖 channel
c := make(chan int)

枚舉類(lèi)型值從1開(kāi)始

在Go中聲明枚舉值的標(biāo)準(zhǔn)方法是使用const包iota。由于變量的默認(rèn)值為0,因此枚舉類(lèi)型的值需要從1開(kāi)始。

Bad Good
type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

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

當(dāng)你需要將0值視為默認(rèn)行為時(shí),枚舉類(lèi)型從0開(kāi)始是有意義的。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

使用time包來(lái)處理時(shí)間

時(shí)間處理很復(fù)雜,關(guān)于時(shí)間錯(cuò)誤預(yù)估有以下這些點(diǎn)。

  1. 一天有24小時(shí)
  2. 一小時(shí)有60分鐘
  3. 一周有7天
  4. 一年有365天
  5. 其他易錯(cuò)點(diǎn)

舉例來(lái)說(shuō), 1 表示在一個(gè)時(shí)間點(diǎn)加上24小時(shí)并不一定會(huì)產(chǎn)生新的一天。

因此,在處理時(shí)間時(shí)應(yīng)始終使用"time"包,因?yàn)樗鼤?huì)用更安全、準(zhǔn)確的方式來(lái)處理這些不正確的假設(shè)。

用 time.Time 表示瞬時(shí)時(shí)間

需要瞬時(shí)時(shí)間語(yǔ)義時(shí),使用time.Time ,在進(jìn)行比較、增加或減少時(shí)間段時(shí),使用time.Time包里的方法。

Bad Good
func isActive(now, start, stop intbool {
  return start <= now && now < stop
}
func isActive(now, start, stop time.Time) bool {
  return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

用 time.Duration 表示時(shí)間段

應(yīng)使用 time.Duration 來(lái)表示時(shí)間段

Bad Good
func poll(delay int) {
  for {
    // ...
    time.Sleep(time.Duration(delay) * time.Millisecond)
  }
}

poll(10) // 這里單位是秒還是毫秒
func poll(delay time.Duration) { // 使用 time.Duration 表示時(shí)間段
  for {
    // ...
    time.Sleep(delay)
  }
}

poll(10*time.Second) // 明確單位

回到剛剛的例子,在一個(gè)瞬時(shí)時(shí)間加上24小時(shí),怎么加這個(gè) "24小時(shí)" 取決于我們的意圖。如果我們想獲取 下一天的當(dāng)前時(shí)間,我們應(yīng)該使用 Time.AddDate。如果我們想獲取比當(dāng)前時(shí)間晚24小時(shí)的瞬時(shí)時(shí)間, 我們應(yīng)該應(yīng)該使用 Time.Add。

newDay := t.AddDate(0 /* years */0 /* months */1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)

對(duì)外交互 使用 time.Time 和 time.Duration

在對(duì)外交互時(shí)盡可能使用 time.Duration 和 time.Time,例如:

  • Command-line 標(biāo)記: flag 通過(guò)支持 time.ParseDuration 來(lái)支持 time.Duration
  • JSON: encoding/json 通過(guò)  [UnmarshalJSON 方法] 支持把 time.Time 解碼為 RFC 3339 字符串
  • SQL: database/sql 支持把 DATETIME 或 TIMESTAMP 類(lèi)型轉(zhuǎn)化為 time.Time
  • YAML: gopkg.in/yaml.v2 支持把 time.Time 作為一個(gè) RFC 3339 字符串, 通過(guò)支持time.ParseDuration 來(lái)支持time.Duration。

如果交互中不支持使用time.Duration,那字段名中應(yīng)包含單位,類(lèi)型應(yīng)為int或float64。

例如, 由于 encoding/json 不支持 time.Duration 類(lèi)型, 因此字段名中應(yīng)包含時(shí)間單位。

Bad Good
// {"interval": 2}
type Config struct {
  Interval int `json:"interval"`
}
// {"intervalMillis": 2000}
type Config struct { // Millis 是單位
  IntervalMillis int `json:"intervalMillis"`
}

如果在交互中不能使用 time.Time,除非有額外約定,否則應(yīng)該使用 string 和 RFC 3339定義的 時(shí)間戳格式。默認(rèn)情況下, Time.UnmarshalText 使用這種格式,并可以通過(guò)time.RFC3339在Time.Format 和 time.Parse 中使用。

盡管在實(shí)踐中這不是什么問(wèn)題,但是你需要記住"time"不能解析閏秒時(shí)間戳(8728),在計(jì)算中也不考慮閏秒(15190)。因此如果你要比較兩個(gè)瞬時(shí)時(shí)間,比較結(jié)果不會(huì)包含這兩個(gè)瞬時(shí)時(shí)間可能會(huì)出現(xiàn)的閏秒。

錯(cuò)誤

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

聲明錯(cuò)誤的選項(xiàng)很少。在為你的代碼選擇合適的用例之前,考慮這些事項(xiàng):

  • 調(diào)用方需要匹配錯(cuò)誤嗎,還是調(diào)用方需要自己處理錯(cuò)誤。如果需要匹配錯(cuò)誤,那應(yīng)該聲明頂級(jí) 錯(cuò)誤類(lèi)型或自定義類(lèi)型 來(lái)讓 errors.Is 或 errors.As 匹配。
  • 錯(cuò)誤信息是靜態(tài)字符串嗎,還是說(shuō)錯(cuò)誤信息是需要上下文的動(dòng)態(tài)字符串。如果是靜態(tài)字符串, 我們可以使用 errors.New,如果是動(dòng)態(tài)字符串,我們應(yīng)該使用fmt.Errorf來(lái) 自定義錯(cuò)誤類(lèi)型。
  • 我們是否正在傳遞由下游返回的新的錯(cuò)誤類(lèi)型,如果是這樣,參考section on error wrapping.
錯(cuò)誤匹配? 錯(cuò)誤信息 使用
無(wú) 靜態(tài) errors.New
無(wú) 動(dòng)態(tài) fmt.Errorf
靜態(tài) errors.New聲明頂級(jí)錯(cuò)誤類(lèi)型
動(dòng)態(tài) 定制 error 類(lèi)型

舉例,使用 errors.New 表示一個(gè)靜態(tài)字符串錯(cuò)誤。如果調(diào)用方需要匹配并處理這個(gè)錯(cuò)誤, 就把這個(gè)錯(cuò)誤聲明為變量來(lái)支持和 errors.Is 匹配。

無(wú)錯(cuò)誤匹配 有錯(cuò)誤匹配
// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

if err := foo.Open(); err != nil {
  //無(wú)法處理錯(cuò)誤
  panic("unknown error")
}
// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // 處理這個(gè)錯(cuò)誤
  } else {
    panic("unknown error")
  }
}

對(duì)于動(dòng)態(tài)字符串的錯(cuò)誤,如果調(diào)用方不需要匹配就是用fmt.Errorf,需要匹配就搞一個(gè)自定義error。

無(wú)錯(cuò)誤匹配 有錯(cuò)誤匹配
// package foo

func Open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
  // Can't handle the error.
  panic("unknown error")
}
// package foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// package bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

注意,如果你的包里導(dǎo)出了錯(cuò)誤變量或錯(cuò)誤類(lèi)型,那這個(gè)錯(cuò)誤將變成你包里公共API的一部分。

錯(cuò)誤包裝

調(diào)用函數(shù)失敗時(shí),有三種選擇供你選擇:

  • 返回原始錯(cuò)誤
  • 用 fmt.Errorf 和 %w 包裝上下文信息
  • 用 fmt.Errorf 和 %v 包裝上下文信息

返回原始錯(cuò)誤不會(huì)附加上下文信息,這樣就保持了原始錯(cuò)誤類(lèi)型和信息。比較適用于lib庫(kù)類(lèi)型代碼 展示底層錯(cuò)誤信息。

如果不是lib庫(kù),就需要增加所需的上下文信息,不然就會(huì)出現(xiàn) "connection refused" 這樣非常 模糊的錯(cuò)誤,理論上應(yīng)該添加上下文,來(lái)得到這樣的報(bào)錯(cuò)信息:"call service foo: connection refused"。

在錯(cuò)誤類(lèi)型上使用 fmt.Errorf 來(lái)添加上下文信息,根據(jù)調(diào)用方不同的使用方式,可以選擇 %w 或 %v 動(dòng)詞。

  • 如果調(diào)用方需要訪問(wèn)底層錯(cuò)誤,使用%w動(dòng)詞,這是一個(gè)用來(lái)包裝錯(cuò)誤的動(dòng)詞,如果你在代碼中使用到了它,請(qǐng)注意 調(diào)用方會(huì)對(duì)此產(chǎn)生依賴(lài),所以當(dāng)你的包裝的錯(cuò)誤是用var聲明的已知類(lèi)型,需要在你的代碼里對(duì)其進(jìn)行測(cè)試。
  • 使用 %v 會(huì)混淆你的底層錯(cuò)誤類(lèi)型,調(diào)用方將無(wú)法進(jìn)行匹配,如果有匹配需求,應(yīng)該使用%w動(dòng)詞。

當(dāng)為返回錯(cuò)誤增加上下文信息時(shí),避免在上下文中增加像 "failed to" 這樣的沒(méi)啥用的短語(yǔ),這樣沒(méi)用的短語(yǔ)在錯(cuò)誤 堆棧中堆積起來(lái)的話,反而不利于你定位bug。

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

然而當(dāng)你的錯(cuò)誤傳給別的系統(tǒng)時(shí),錯(cuò)誤信息應(yīng)該足夠清晰。(比如, 錯(cuò)誤信息在日志中以 "Failed" 開(kāi)頭)

其他參考信息:Don't just check errors, handle them gracefully.

錯(cuò)誤命名

對(duì)于全局變量類(lèi)型,根據(jù)是否導(dǎo)出使用  Err 或 err 前綴。詳情參考:Prefix Unexported Globals with _.

var (
  // 以下兩個(gè)錯(cuò)誤是導(dǎo)出類(lèi)型,所以他們的命名以 Err 作為開(kāi)頭,用戶可以使用 errors.Is 來(lái)匹配錯(cuò)誤類(lèi)型
  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // 這個(gè)錯(cuò)誤是非導(dǎo)出類(lèi)型,不會(huì)作為我們公共API的一部分,但是你可以在包內(nèi)使用errors.Is匹配它。
  errNotFound = errors.New("not found")
)

對(duì)于自定義錯(cuò)誤類(lèi)型,請(qǐng)使用Error后綴。

// 這個(gè)錯(cuò)誤是被導(dǎo)出的,用戶可以使用errors.As去匹配它。
type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// 這個(gè)錯(cuò)誤是非導(dǎo)出類(lèi)型,不會(huì)作為我們公共API的一部分,但是你可以在包內(nèi)使用errors.As匹配它。 

type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

todo:errors.Is和errors.As的區(qū)別

處理斷言失敗

在不正確的類(lèi)型斷言上 使用單返回值來(lái)處理會(huì)導(dǎo)致 panic, 因此請(qǐng)使用 "comma ok" 習(xí)俗.

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

不要使用Panic

在生產(chǎn)環(huán)境的業(yè)務(wù)代碼避免使用panic。Panics 是級(jí)聯(lián)問(wèn)題cascading failures的主要來(lái)源。如果發(fā)生錯(cuò)誤,函數(shù)必須返回錯(cuò)誤,讓調(diào)用方?jīng)Q定如何處理這種情況。

Bad Good
func run(args []string) {
  if len(args) == 0 {
    panic("an argument is required")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}
func run(args []string) error {
  if len(args) == 0 {
    return errors.New("an argument is required")
  }
  // ...
  return nil
}

func main() {
  if err := run(os.Args[1:]); err != nil {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
  }
}

Panic/recover 不是錯(cuò)誤處理策略。當(dāng)系統(tǒng)發(fā)生像空指針異常這種 不可恢復(fù)的 Fatal 異常時(shí),才需要使用Panic。唯一的意外情況是項(xiàng)目啟動(dòng):如果程序啟動(dòng)階段出現(xiàn)問(wèn)題需要拋出異常。

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

即使在測(cè)試中,優(yōu)先使用t.Fatal 或 t.FailNow 而不是異常,來(lái)確保失敗情況被記錄。

Bad Good
// func TestFoo(t *testing.T)

f, err := os.CreateTemp("""test")
if err != nil {
  panic("failed to set up test")
}
// func TestFoo(t *testing.T)

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

使用 go.uber.org/atomic

使用 sync/atomic 包的原子操作對(duì)數(shù)據(jù)類(lèi)型進(jìn)行操作(int32, int64, 等.) 。因?yàn)樽约翰僮魅菀淄浭褂迷硬僮鲗?duì)變量進(jìn)行讀取和修改。

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

Bad Good
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!
}
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()
}

避免使用全局可變對(duì)象

Avoid mutating global variables, instead opting for dependency injection. This applies to function pointers as well as other kinds of values.

Bad Good
// sign.go

var _timeNow = time.Now

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

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

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

func (s *signer) Sign(msg stringstring {
  now := s.now()
  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))
}
// sign_test.go

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

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

避免在公共結(jié)構(gòu)體中內(nèi)嵌類(lèi)型

嵌入類(lèi)型會(huì)暴露實(shí)現(xiàn)細(xì)節(jié),無(wú)法類(lèi)型演化,讓文檔也變得模糊。

假設(shè)你用AbstractList結(jié)構(gòu)體實(shí)現(xiàn)了公共的 list 方法,避免在其他實(shí)現(xiàn)中內(nèi)嵌AbstractList類(lèi)型。而是應(yīng)該在其他結(jié)構(gòu)體中顯式聲明list,并在方法實(shí)現(xiàn)中調(diào)用list的方法。

type AbstractList struct {}

// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
  // ...
}

// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
  // ...
}
Bad Good
// ConcreteList is a list of entities.
type ConcreteList struct {
  *AbstractList
}
// ConcreteList is a list of entities.
type ConcreteList struct {
  list *AbstractList
}

// Add adds an entity to the list.
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}

// Remove removes an entity from the list.
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

Go允許內(nèi)嵌類(lèi)型type embedding作為組合和繼承的折中方案。外部的結(jié)構(gòu)體會(huì)獲得內(nèi)嵌類(lèi)型的隱式拷貝。默認(rèn)情況下,內(nèi)嵌類(lèi)型的方法會(huì)嵌入實(shí)例的同一方法。

外部的結(jié)構(gòu)體會(huì)獲取嵌入類(lèi)型的同名字段。如果嵌入類(lèi)型的字段是公開(kāi)(public)的,那嵌入后也是公開(kāi)的。為保證向后兼容性,外部結(jié)構(gòu)體未來(lái)每個(gè)版本都需要保留嵌入類(lèi)型。

很少場(chǎng)景需要嵌入類(lèi)型,雖然嵌入類(lèi)型很方便,讓你避免編寫(xiě)冗長(zhǎng)的方法。

即使是用interface而不是結(jié)構(gòu)體來(lái)嵌入方法,這是給開(kāi)發(fā)人員帶來(lái)了一定的靈活性,但是仍然暴露了具體實(shí)現(xiàn)列表的抽象細(xì)節(jié)。

Bad Good
// AbstractList is a generalized implementation
// for various kinds of lists of entities.
type AbstractList interface {
  Add(Entity)
  Remove(Entity)
}

// ConcreteList is a list of entities.
type ConcreteList struct {
  AbstractList
}
// ConcreteList is a list of entities.
type ConcreteList struct {
  list AbstractList
}

// Add adds an entity to the list.
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}

// Remove removes an entity from the list.
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

不管是嵌入結(jié)構(gòu)體還是嵌入接口,都會(huì)限制類(lèi)型的演化。

  • 若嵌入接口,當(dāng)你增加一個(gè)方法是一種破壞性改變
  • 若嵌入結(jié)構(gòu)體,當(dāng)你刪除一個(gè)方法是一種破壞性改變
  • 刪除嵌入類(lèi)型是一種破壞性改變
  • 即使用滿足接口約束的類(lèi)型去替換嵌入類(lèi)型,也是一種破壞性改變

盡管編寫(xiě)內(nèi)嵌類(lèi)型已實(shí)現(xiàn)的方法是乏味的。但是這些工作隱藏了實(shí)現(xiàn)細(xì)節(jié),留下了更多更改的機(jī)會(huì), 并消除了在文檔中發(fā)現(xiàn)完整List接口的間接方法。

避免使用內(nèi)建命名

Go語(yǔ)言的spec中列舉了一些內(nèi)建命名,在你的Go程序中應(yīng)該避免使用預(yù)聲明的標(biāo)識(shí)符;

根據(jù)上下文的不同,用預(yù)聲明標(biāo)識(shí)符命名變量可能會(huì)在當(dāng)前作用域下覆蓋官方標(biāo)識(shí)符,讓你的代碼變得難以理解。最好的情況下,編譯器會(huì)直接報(bào)錯(cuò),最糟糕的情況下,這樣的代碼會(huì)引入難以排查的bug。

Bad Good
var error string
// `error` 覆蓋了內(nèi)建的error

// or

func handleErrorMessage(error string) {
    // `error` 覆蓋了內(nèi)建的error
}
var errorMessage string
// `error` 指向內(nèi)置的 error 

// or

func handleErrorMessage(msg string) {
    // `error` 指向內(nèi)置的 error
}
type Foo struct {
    // While these fields technically don't
    // constitute shadowing, grepping for
    // `error` or `string` strings is now
    // ambiguous.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` and `f.error` are
    // visually similar
    return f.error
}

func (f Foo) String() string {
    // `string` and `f.string` are
    // visually similar
    return f.string
}
type Foo struct {
    // `error` and `string` strings are
    // now unambiguous.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

注意當(dāng)你使用預(yù)聲明標(biāo)識(shí)符時(shí)編譯器不會(huì)報(bào)錯(cuò),但是像 go vet 這樣的工具會(huì)告訴你標(biāo)識(shí)符被覆蓋的情況。

避免使用init()

盡可能避免使用init()。如果實(shí)在依賴(lài) init(),可以使用以下方式:

  1. 不管程序環(huán)境或調(diào)用方式如何,初始化要完全確定。
  2. 避免依賴(lài)其他init()函數(shù)的順序或者產(chǎn)生的結(jié)果。雖然init()順序是明確的,但是代碼可以更改。init()函數(shù)之間的關(guān)系會(huì)讓代碼變得易錯(cuò)和脆弱。
  3. 避免讀寫(xiě)全局變量、環(huán)境變量,比如機(jī)器信息、環(huán)境變量、工作目錄,程序的參數(shù)和輸入等等。
  4. 避免 I/O 操作,比如文件系統(tǒng),網(wǎng)絡(luò)和系統(tǒng)調(diào)用。

如果代碼不能滿足這些需求,那可能屬于幫助代碼,需要作為main()函數(shù)的一部分進(jìn)行調(diào)用(或者封裝初始化邏輯,讓main 函數(shù)去調(diào)用)。需要注意的是,被其他模塊依賴(lài)的代碼應(yīng)該完全指定初始化順序的確定性,而不是依賴(lài)"初始化魔法"。

Bad Good
type Foo struct {
    // ...
}

var _defaultFoo Foo

func init() {
    _defaultFoo = Foo{
        // ...
    }
}
var _defaultFoo = Foo{
    // ...
}

// or, better, for testability:

var _defaultFoo = defaultFoo()

func defaultFoo() Foo {
    return Foo{
        // ...
    }
}
type Config struct {
    // ...
}

var _config Config

func init() {
    // Bad: based on current directory
    cwd, _ := os.Getwd()

    // Bad: I/O
    raw, _ := os.ReadFile(
        path.Join(cwd, "config""config.yaml"),
    )

    yaml.Unmarshal(raw, &_config)
}
type Config struct {
    // ...
}

func loadConfig() Config {
    cwd, err := os.Getwd()
    // handle err

    raw, err := os.ReadFile(
        path.Join(cwd, "config""config.yaml"),
    )
    // handle err

    var config Config
    yaml.Unmarshal(raw, &config)

    return config
}

但是在某些情況下,init()函數(shù)可能更具優(yōu)勢(shì):

  • 單個(gè)賦值語(yǔ)句中無(wú)法表示的復(fù)雜表達(dá)式
  • 插件鉤子,比如 database/sql,編碼信息注冊(cè)表等
  • 對(duì) Google Cloud Functions 和其他形式確定性預(yù)計(jì)算的優(yōu)化

優(yōu)雅退出主函數(shù)

Go程序使用os.Exit 或 log.Fatal*來(lái)立即退出。(Panic 不是優(yōu)雅的程序退出方式,可以參考 don't panic)

應(yīng)該只在main()函數(shù)里調(diào)用os.Exit 或 log.Fatal*函數(shù)。其他函數(shù)應(yīng)該返回錯(cuò)誤來(lái)表示失敗,在main中進(jìn)行退出。

Bad Good
func main() {
  body := readFile(path)
  fmt.Println(body)
}

func readFile(path stringstring {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  }

  b, err := io.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  return string(b)
}
func main() {
  body, err := readFile(path)
  if err != nil {
    log.Fatal(err)
  }
  fmt.Println(body)
}

func readFile(path string) (string, error) {
  f, err := os.Open(path)
  if err != nil {
    return "", err
  }

  b, err := io.ReadAll(f)
  if err != nil {
    return "", err
  }

  return string(b), nil
}

程序中多個(gè)函數(shù)都能退出的話會(huì)有一些問(wèn)題:

  • 不明顯的控制流:多個(gè)函數(shù)都能退出的話,找出程序的控制流會(huì)變得困難。
  • 測(cè)試?yán)щy:如果一個(gè)函數(shù)讓程序退出,那它也會(huì)讓測(cè)試退出。這樣會(huì)讓函數(shù)難以測(cè)試。而且可能會(huì)讓go text無(wú)法測(cè)試其他函數(shù)。
  • 跳過(guò)清理:當(dāng)一個(gè)函數(shù)退出程序時(shí),會(huì)跳過(guò)已經(jīng)進(jìn)入defer隊(duì)列的函數(shù)調(diào)用。這樣會(huì)增加跳過(guò)清理任務(wù)的風(fēng)險(xiǎn)。

一次性退出

有條件的情況下,main()函數(shù)中最好只調(diào)用os.Exit 或 log.Fatal 一次。如果有多種錯(cuò)誤情況會(huì)停止 程序的執(zhí)行,將這些錯(cuò)誤放在一個(gè)獨(dú)立的函數(shù)中,并返回錯(cuò)誤,main()中處理錯(cuò)誤并退出。

把所有的關(guān)鍵邏輯放在一個(gè)獨(dú)立的可測(cè)試的函數(shù)中,會(huì)讓你的main()函數(shù)變得簡(jiǎn)短。

Bad Good
package main

func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("missing file")
  }
  name := args[0]

  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()

  // If we call log.Fatal after this line,
  // f.Close will not be called.

  b, err := io.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  // ...
}
package main

func main() {
  if err := run(); err != nil {
    log.Fatal(err)
  }
}

func run() error {
  args := os.Args[1:]
  if len(args) != 1 {
    return errors.New("missing file")
  }
  name := args[0]

  f, err := os.Open(name)
  if err != nil {
    return err
  }
  defer f.Close()

  b, err := io.ReadAll(f)
  if err != nil {
    return err
  }

  // ...
}

在序列化結(jié)構(gòu)體中使用字段標(biāo)簽。

要編碼成JSON、YAML或其他支持tag格式的結(jié)構(gòu)體字段應(yīng)該用指定對(duì)應(yīng)項(xiàng)tag標(biāo)簽進(jìn)行注釋。

Bad Good
type Stock struct {
  Price int
  Name  string
}

bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})
type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // Safe to rename Name to Symbol.
}

bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

Rationale: 結(jié)構(gòu)體的序列化方式是不同系統(tǒng)通信的契約。修改結(jié)構(gòu)體的結(jié)構(gòu)和字段會(huì)破壞這個(gè)契約。在結(jié)構(gòu)體中聲明tag 可以防止重構(gòu)結(jié)構(gòu)體中意外違反約定。

性能

性能方面的指導(dǎo)準(zhǔn)則只適用于高頻調(diào)用場(chǎng)景。

使用 strconv 而不是 fmt

當(dāng)需要原始類(lèi)型和字符串互相轉(zhuǎn)化時(shí),strconvfmt性能更好。

Bad Good
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

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

不要在for循環(huán)中創(chuàng)建[]byte類(lèi)型,應(yīng)該在for循環(huán)開(kāi)始前把[]byte數(shù)據(jù)準(zhǔn)備好。

Bad Good
for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

預(yù)先指定容器類(lèi)型的容量

盡可能指定容器類(lèi)型變量的容量來(lái)預(yù)先分配容器類(lèi)型所需的內(nèi)存大小。這樣可以預(yù)防由于后續(xù)由分配元素 (由于拷貝或重新指定容器大?。┒鴮?dǎo)致的內(nèi)存分配。

指定Map容量

如果有可能,用make來(lái)初始化map類(lèi)型,并指定map的大小。

make(map[T1]T2, hint)

使用 make() 初始化map時(shí),提供一個(gè)容量來(lái)執(zhí)行size,這樣會(huì)減少后續(xù)將給map添加元素時(shí)引起的內(nèi)存分配。

注意,和 slice 不同,給map指定容量不意味著搶占式內(nèi)存分配完成,而是會(huì)用于預(yù)估的哈希表內(nèi)部 buckets。因此,當(dāng)你給 map 添加元素,或者給 map 指定值時(shí),仍有可能發(fā)生內(nèi)存分配。

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

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

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

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

m 沒(méi)有指定內(nèi)存大小,因此在運(yùn)行期間可能會(huì)有更多的內(nèi)存分配。

m 指定了內(nèi)存大小,因此在運(yùn)行期間可能會(huì)有較少的內(nèi)存分配。

指定Slice容量

如果有可能的話,在使用make()初始化slice的時(shí)候提供容量大小,尤其是后面需要 append 操作時(shí)。

make([]T, length, capacity)

和 map 不同,slice的容量不是一個(gè)提示:編譯器會(huì)根據(jù) make() 提供的容量信息申請(qǐng)足夠的內(nèi)存, 這意味著后續(xù)的 append() 操作不會(huì)申請(qǐng)內(nèi)存(除非slice的長(zhǎng)度和容量相等,這樣的話后續(xù)添加元素 會(huì)申請(qǐng)內(nèi)存來(lái)調(diào)整 slice 的大?。?。

Bad Good
for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
BenchmarkBad-4    100000000    2.48s
BenchmarkGood-4   100000000    0.21s

規(guī)范

避免代碼過(guò)長(zhǎng)

避免由于長(zhǎng)度過(guò)長(zhǎng)而需要水平滾動(dòng)或者太需要轉(zhuǎn)動(dòng)頭部的代碼。

我們建議一行代碼長(zhǎng)度為 99個(gè)字符,如果代碼超過(guò)了這個(gè)限制就應(yīng)該換行。但是這也不是絕對(duì)的,代碼 可以超過(guò)這個(gè)限制。

一致性

本文的一些指導(dǎo)準(zhǔn)則可以被客觀評(píng)估,其他準(zhǔn)則可以根據(jù)實(shí)際情況進(jìn)行選擇。

但是最重要是,在你的代碼中要保持一致。

一致性的代碼更易于維護(hù),更容易合理化,需要的認(rèn)知開(kāi)銷(xiāo)較少;當(dāng)新的管理出現(xiàn)時(shí)或 bug 被修復(fù)后也更易于 遷移和更新。

與之相反,如果單個(gè)代碼庫(kù)中有多種沖突的風(fēng)格,會(huì)讓維護(hù)成本升高、不確定性增高、認(rèn)知不協(xié)調(diào),這些問(wèn)題會(huì) 導(dǎo)致開(kāi)發(fā)效率降低,code review 困難,且容易產(chǎn)生 bug。

當(dāng)你在代碼庫(kù)中實(shí)施標(biāo)準(zhǔn)時(shí),建議最低在包層面進(jìn)行修改:在子包層面進(jìn)行應(yīng)用違反了上述約定,因?yàn)樵谝环N代碼 里面引入了多種風(fēng)格。

相似聲明放一組

Go語(yǔ)言支持組引用。

Bad Good
import "a"
import "b"
import (
  "a"
  "b"
)

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

Bad Good

const a = 1
const b = 2



var a = 1
var b = 2



type Area float64
type Volume float64
const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)

注意只把相關(guān)的變量聲明到一個(gè)組里,不想管的聲明放在多個(gè)組里。

Bad Good
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
  EnvVar = "MY_ENV"
)
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const EnvVar = "MY_ENV"

組聲明不限制在哪使用。比如,你可以在函數(shù)中使用組聲明。

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

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

  // ...
}

例外:對(duì)于變量聲明,尤其是函數(shù)中的變量聲明,不管他們之間是否有關(guān)系,都應(yīng)該被放在一個(gè)組內(nèi)。

Bad Good
func (c *client) request() {
  caller := c.name
  format := "json"
  timeout := 5*time.Second
  var err error

  // ...
}
func (c *client) request() {
  var (
    caller  = c.name
    format  = "json"
    timeout = 5*time.Second
    err error
  )

  // ...
}

包導(dǎo)入順序

包中應(yīng)該有兩種導(dǎo)入順序:

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

默認(rèn)情況下,應(yīng)該使用 goimports 的導(dǎo)入順序。

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

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

包命名

當(dāng)命名包時(shí),應(yīng)按照下面原則命名:

  • 全小寫(xiě)字母。無(wú)大寫(xiě)字母或下劃線。
  • 大多數(shù)導(dǎo)入包的情況下,不需要對(duì)包重新命名。
  • 簡(jiǎn)短而簡(jiǎn)潔,因?yàn)楫?dāng)你使用包名時(shí)你都需要完成輸入包名稱(chēng)。
  • 不要使用復(fù)數(shù)。比如:命名為 net/url, 而不是 net/urls。
  • 不要使用"common", "util", "shared", 或 "lib"。這些包含有信息太少了。

可以參考 Package Names 和 Style guideline for Go packages.

函數(shù)命名

我們遵守 Go 社區(qū) MixedCaps for function names 約定。一種其他情況是使用測(cè)試函數(shù)。測(cè)試函數(shù) 命名可以包含下劃線以便于相關(guān)測(cè)試函數(shù)進(jìn)行分組。比如:TestMyFunction_WhatIsBeingTested。

導(dǎo)入別名

如果包名稱(chēng)和導(dǎo)入路徑最后一個(gè)元素不匹配,就需要使用導(dǎo)入別名。

import (
  "net/http"

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

其他情況下,除非幾個(gè)包之間有導(dǎo)入沖突,否則應(yīng)該避免使用導(dǎo)入別名。

Bad Good
import (
  "fmt"
  "os"


  nettrace "golang.net/x/trace"
)
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

函數(shù)分組和排序

  • 函數(shù)應(yīng)該按照大概調(diào)用順序排序。
  • 一個(gè)文件中的函數(shù)應(yīng)該按照接收者分組。

因此,導(dǎo)入的函數(shù)時(shí),應(yīng)該放在 structconstvar 的下面。

像 newXYZ()/NewXYZ() 這樣的函數(shù)可能會(huì)出現(xiàn)在類(lèi)型定義下、接收者的其他方法之上。

由于函數(shù)是按照接收者進(jìn)行分組的,普通的工具函數(shù)應(yīng)該放在文件末尾。

Bad Good
func (s *something) Cost() {
  return calcCost(s.weights)
}

type something struct{ ... }

func calcCost(n []intint {...}

func (s *something) Stop() {...}

func newSomething() *something {
    return &something{}
}
type something struct{ ... }

func newSomething() *something {
    return &something{}
}

func (s *something) Cost() {
  return calcCost(s.weights)
}

func (s *something) Stop() {...}

func calcCost(n []intint {...}

減少嵌套

代碼應(yīng)通過(guò)盡早處理錯(cuò)誤/特殊情況盡早處理/循環(huán)中使用 continue 等手段,來(lái)減少嵌套代碼過(guò)多問(wèn)題。

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

沒(méi)用的else

如啊a變量在兩個(gè)if分支中都進(jìn)行賦值操作,則可以被替換為只在一個(gè)if分支中聲明。

Bad Good
var a int
if b {
  a = 100else {
  a = 10
}
a := 10
if b {
  a = 100
}

頂層變量聲明

在頂層使用var來(lái)聲明變量。不要指定類(lèi)型,除非它和表達(dá)式的類(lèi)型不同。

Bad Good
var _s string = F()

func F() string { return "A" }
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" }

如果表達(dá)式的類(lèi)型和所需類(lèi)型不一樣,需要指定類(lèi)型。

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.

非導(dǎo)出變量使用_前綴

對(duì)于非導(dǎo)出類(lèi)型的變量,在用vars and const聲明時(shí)加上_前綴,來(lái)表示他們是全局符號(hào)。

原因:頂層聲明的變量作用域一般是包范圍。用一個(gè)常見(jiàn)的名字可能會(huì)導(dǎo)致在其他包中被意外修改。

Bad Good
// 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.
}
// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

異常:非導(dǎo)出的錯(cuò)誤類(lèi)型一般使用不帶 _ 的 err 前綴。參考Error Naming.

結(jié)構(gòu)體內(nèi)嵌類(lèi)型

嵌入類(lèi)型應(yīng)該放在結(jié)構(gòu)體的最上面,應(yīng)該和結(jié)構(gòu)體的常規(guī)字段用一個(gè)空行分隔開(kāi)。

Bad Good
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

內(nèi)嵌類(lèi)型會(huì)帶來(lái)足夠的好處,比如在語(yǔ)義上會(huì)增加或增強(qiáng)功能。但應(yīng)該在對(duì)用戶沒(méi)有影響的情況下使用內(nèi)嵌。(參考: 避免在公共結(jié)構(gòu)中嵌入類(lèi)型).

例外:即使是未導(dǎo)出類(lèi)型,Mutex 也不應(yīng)該被內(nèi)嵌。參考:: Mutex的零值是有效的.

這些情況下避免內(nèi)嵌:

  • 單純?yōu)榱吮憷兔烙^。
  • 讓外部類(lèi)型構(gòu)造起來(lái)或使用起來(lái)更困難。
  • 影響了外部的零值。如果外部類(lèi)型的零值是有用的,嵌入類(lèi)型應(yīng)該也有一個(gè)有用的零值。
  • 作為嵌入類(lèi)型的副作用,公開(kāi)外部類(lèi)型的不相關(guān)函數(shù)或字段。
  • 公開(kāi)非導(dǎo)出類(lèi)型。
  • 影響外部類(lèi)型的復(fù)制語(yǔ)義。
  • 影響外部類(lèi)型的API或類(lèi)型語(yǔ)義。
  • 影響內(nèi)部類(lèi)型的非規(guī)范形式。
  • 公開(kāi)外部類(lèi)型的詳細(xì)實(shí)現(xiàn)信息。
  • 允許用戶觀察和控制內(nèi)部類(lèi)型。
  • 通過(guò)包裝的形式改變了內(nèi)部函數(shù)的行為,這種包裝的方式會(huì)給用戶造成意外觀感。

簡(jiǎn)單概括,使用嵌入類(lèi)型時(shí)要明確目的。一個(gè)不錯(cuò)的方式是:"這些嵌入的字段/方法是否需要被直接添加到外部 類(lèi)型",如果答案是"一些"或者"No",不要使用內(nèi)嵌類(lèi)型,而是使用命名字段。

Bad Good
type A struct {
    // Bad: A.Lock() and A.Unlock() are
    //      now available, provide no
    //      functional benefit, and allow
    //      users to control details about
    //      the internals of A.
    sync.Mutex
}
type countingWriteCloser struct {
    // Good: Write() is provided at this
    //       outer layer for a specific
    //       purpose, and delegates work
    //       to the inner type's Write().
    io.WriteCloser

    count int
}

func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}
type Book struct {
    // Bad: pointer changes zero value usefulness
    io.ReadWriter

    // other fields
}

// later

var b Book
b.Read(...)  // panic: nil pointer
b.String()   // panic: nil pointer
b.Write(...) // panic: nil pointer
type Book struct {
    // Good: has useful zero value
    bytes.Buffer

    // other fields
}

// later

var b Book
b.Read(...)  // ok
b.String()   // ok
b.Write(...) // ok
type Client struct {
    sync.Mutex
    sync.WaitGroup
    bytes.Buffer
    url.URL
}
type Client struct {
    mtx sync.Mutex
    wg  sync.WaitGroup
    buf bytes.Buffer
    url url.URL
}

本地變量聲明

如果將變量聲明為某個(gè)值,應(yīng)該使用短變量命名方式::=

Bad Good
var s = "foo"
s := "foo"

然而,有些情況下用 var 會(huì)讓聲明語(yǔ)句更加清晰,比如聲明空slice.

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

nil值的slice有效

nil 代表長(zhǎng)度為0的有效slice, 意味著:

  • 你不應(yīng)該聲明一個(gè)長(zhǎng)度為0的空slice,而是用nil來(lái)代替。

    Bad Good
    if x == "" {
      return []int{}
    }
    
    if x == "" {
      return nil
    }
    
  • 要檢查slice是否為空,不應(yīng)該檢查 nil, 而是用長(zhǎng)度判斷len(s) == 0。

    Bad Good
    func isEmpty(s []stringbool {
      return s == nil
    }
    
    func isEmpty(s []stringbool {
      return len(s) == 0
    }
    
  • 用 var 聲明的零值slice是有效的,沒(méi)必要用 make 來(lái)創(chuàng)建。

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

另外記住,雖然 nil 的slice有效,但是它不等于長(zhǎng)度為0的 slice。在一些情況下(比如說(shuō)序列化), 這兩種slice的表現(xiàn)不同。

縮小變量作用域

盡可能減小變量的作用域。如果與 減少嵌套 沖突,就不要縮小。

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

但是如果作用域是 if 范圍之外,不應(yīng)該減少作用域。

Bad Good
if data, err := os.ReadFile(name); err == nil {
  err = cfg.Decode(data)
  if err != nil {
    return err
  }

  fmt.Println(cfg)
  return nilelse {
  return err
}
data, err := os.ReadFile(name)
if err != nil {
   return err
}

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

fmt.Println(cfg)
return nil

不面參數(shù)語(yǔ)義不明確

在函數(shù)中裸傳參數(shù)值會(huì)讓代碼語(yǔ)義不明確,可以添加 C 風(fēng)格(/* ... */)的注釋。

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

printInfo("foo"truetrue)
// func printInfo(name string, isLocal, done bool)

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

當(dāng)然,更好的處理方式將上面的 bool 換成自定義類(lèi)型。因?yàn)槲磥?lái)可能不僅僅局限于兩個(gè)bool值(true/false)。

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status = iota + 1
  StatusDone
  // 獲取未來(lái)會(huì)有 StatusInProgress 枚舉
)

func printInfo(name string, region Region, status Status)

字符串中避免轉(zhuǎn)義

Go中支持 字符串原始值,當(dāng)需要 轉(zhuǎn)義時(shí),盡量使用 "`" 來(lái)包裝字符串。

Bad Good
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

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

初始化結(jié)構(gòu)體時(shí)聲明字段名

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

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

例外:當(dāng)有 3 個(gè)或更少的字段時(shí),測(cè)試表中的字段名也許可以省略。

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

省略結(jié)構(gòu)體中的零值字段

當(dāng)初始化結(jié)構(gòu)體字段時(shí),除非需要提供一個(gè)有意義的上下文,否則需要忽略對(duì)零值字段進(jìn)行賦值。因?yàn)镚o 會(huì)自動(dòng)給這些零值字段進(jìn)行填充。

Bad Good
user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}
user := User{
  FirstName: "John",
  LastName: "Doe",
}

這種行為讓我們忽略了上下文無(wú)關(guān)的噪音信息。只關(guān)注有意義的特殊值。

當(dāng)零值代表有意義的上下文時(shí)需要提供零值。比如在  表驅(qū)動(dòng)測(cè)試 中零值字段 是有意義的。

tests := []struct{
  give string
  want int
}{
  {give: "0", want: 0},
  // ...
}

空結(jié)構(gòu)體用var聲明

當(dāng)結(jié)構(gòu)體中所有的字段都為空時(shí),用 var 來(lái)聲明結(jié)構(gòu)體。

Bad Good
user := User{}
var user User

這種 零值結(jié)構(gòu)體 和具有非零值字段的結(jié)構(gòu)體有所不同,和 map初始化 更相似, 和我們更想用的 [聲明空Slices][聲明空Slices] 更匹配。

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

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

Bad Good
sval := T{Name: "foo"}

// 非一致的
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

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

初始化Map

優(yōu)先使用make來(lái)創(chuàng)建空map,這樣使得map的初始化不同于聲明,而且你還可以在 make 中添加map的大小提示。

Bad Good
var (
  // m1 的讀寫(xiě)操作都是安全的
  // m2 的寫(xiě)操作會(huì)panic
  m1 = map[T1]T2{}
  m2 map[T1]T2
)
var (
  // m1 的讀寫(xiě)操作都是安全的
  // m2 的寫(xiě)操作會(huì)panic
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

聲明和初始化在形式上相似

聲明和初始化在形式上隔離

盡可能在 make() 中制定map的初始化容量,可以參考:Specifying Map Capacity Hints。

另外,如果map初始化的時(shí)候需要賦值固定信息,使用 map literals 方式來(lái)初始化。

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

原則上:在初始化map時(shí)增加一組固定的元素,就使用map literals。否則就使用 make(如果可以, 盡可能指定map的容量)。

在Printf外面格式化字符串

如果你在函數(shù)外聲明 Printf 風(fēng)格 函數(shù)的格式字符串,請(qǐng)將其設(shè)置為 const 常量。

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

Bad Good
msg := "unexpected values %v%v\n"
fmt.Printf(msg, 12)
const msg = "unexpected values %v%v\n"
fmt.Printf(msg, 12)

命名Printf樣式函數(shù)

使用Printf函數(shù)時(shí),應(yīng)保證go vet可以檢測(cè)到他的格式化字符串。

這意味著你需要使用預(yù)定義的Printf函數(shù)名稱(chēng),go vet會(huì)默認(rèn)檢查這些。更多信息,請(qǐng)參考:Printf family

如果不能使用預(yù)定義的名稱(chēng),請(qǐng)以 f 結(jié)束選擇的名稱(chēng):Wrapf,而不是Wrap。go vet可以要求檢查特定的 Printf樣式名稱(chēng),但名稱(chēng)必須以f結(jié)尾。

go vet -printfuncs=wrapf,statusf

參考 go vet: Printf family check.

Patterns

Test Tables

當(dāng)你的測(cè)試用例形式上重復(fù)時(shí),用 subtests 方式編寫(xiě)case會(huì)讓測(cè)試用例看起來(lái)更加簡(jiǎn)潔。

Bad Good
// 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)
// 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的方式,在拓展測(cè)試用例時(shí)也會(huì)顯得更加清晰。

我們遵守這樣的準(zhǔn)則:搞一個(gè)slice類(lèi)型的struct測(cè)試用例,每個(gè)測(cè)試case叫做tt。然后使用givewant說(shuō)明測(cè)試用例的輸入和輸出。

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

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

對(duì)于并行測(cè)試,比如一些特殊的循環(huán) (比如那些生產(chǎn) goroutine 或 在循環(huán)中捕獲引用的循環(huán)), 需要 注意在循環(huán)中明確分配循環(huán)變量來(lái)確保不會(huì)產(chǎn)生閉包。

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

for _, tt := range tests {
  tt := tt // for t.Parallel
  t.Run(tt.give, func(t *testing.T) {
    t.Parallel()
    // ...
  })
}

在上面的例子中,由于循環(huán)中使用了 t.Parallel(),我們必須在外部循環(huán)中聲明一個(gè) tt 變量。如果不這么做,大多數(shù)測(cè)試用例都會(huì)收到一個(gè)非預(yù)期的 tt,或是一個(gè)在運(yùn)行期改變的值。

函數(shù)功能選項(xiàng)API

功能選項(xiàng)是一種模式,你可以聲明一個(gè)對(duì)用戶不透明的 Option 類(lèi)型,在一些內(nèi)部結(jié)構(gòu)中記錄信息。函數(shù)接收不定長(zhǎng)的參數(shù)選項(xiàng),并根據(jù)參數(shù)做不同的行為。

對(duì)于需要拓展參數(shù)的構(gòu)造方法或是其他需要可選參數(shù)的公共API可以考慮這種模式,對(duì)于參數(shù)在三個(gè)及以上 的函數(shù)更應(yīng)該考慮。

Bad Good
// package db

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

type Option interface {
  // ...
}

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

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

// Open 創(chuàng)建一個(gè)連接
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  // ...
}

即使用戶默認(rèn)不需要 cache 和 logger,也需要提供這倆參數(shù)。

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)

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)方式是 提供一個(gè) Option 接口,里面有一個(gè)非導(dǎo)出類(lèi)型方法,在一個(gè)非 導(dǎo)出類(lèi)型的 options 結(jié)構(gòu)體中記錄選項(xiàng)。

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 創(chuàng)建一個(gè)連接
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)這種方法的模式,但我們認(rèn)為上面提供的這種模式給作者提供了更高的靈活性,更 易于調(diào)試和測(cè)試。這種方式可以在測(cè)試和mock中進(jìn)行比較,而閉包方式難以做到。此外,它允許 option 實(shí)現(xiàn)其他接口,比如 fmt.Stringer,會(huì) string 類(lèi)型的可讀性更高。

還可以參考:

  • Self-referential functions and the design of options

  • Functional options for friendly APIs

Linting

比其他任何 "神圣" linter 工具更重要的是,在你的代碼庫(kù)里使用一致性的 lint 工具。

我們建議最少要使用下面這些 linters 工具嗎,因?yàn)槲覀冋J(rèn)為這些工具可以幫你捕獲最常見(jiàn)的問(wèn)題,有助于 在沒(méi)有規(guī)定的前提下提高代碼質(zhì)量:

  • errcheck 確保錯(cuò)誤被處理

  • goimports 格式化代碼和管理包引用

  • golint 指出常見(jiàn)的文本錯(cuò)誤

  • govet 分析代碼中的常見(jiàn)錯(cuò)誤

  • staticcheck 各種靜態(tài)分析檢查

Lint Runners

由于優(yōu)秀的性能表現(xiàn),我們推薦 golangci-lint 作為Go代碼的首選 lint 工具。這個(gè)倉(cāng)庫(kù)有在一個(gè).golangci.yml 例子,里面有配置的 linters 工具和設(shè)置。

golangci-lint 有一系列 various linters 可供使用。建議將這些 linters 作為基礎(chǔ)集合, 我們鼓勵(lì)團(tuán)隊(duì)內(nèi)部將其他有意義的 linters 工具在他們的項(xiàng)目中進(jìn)行使用。


3 人點(diǎn)贊