App下載

Uber 的 Go 規(guī)范

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

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

介紹

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

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

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

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

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

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

  • 保存時運行 goimports
  • 運行 golint 和 go vet 檢查錯誤

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

指南

指向interface的指針

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

interface 類型包括兩部分:

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

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

驗證接口合法性

在編譯期驗證接口的合法性,需要驗證的有:

  • 驗證導(dǎo)出類型在作為API時是否實現(xiàn)了特定接口
  • 實現(xiàn)一個接口的導(dǎo)出和非導(dǎo)出類型是集合的一部分
  • 違反接口合理性無法編譯通過,通知用戶
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沒有實現(xiàn)http.Handler接口,那么var _ http.Handler = (*Handler)(nil)語句在編譯期就會報錯;

賦值語句的右邊部門應(yīng)是斷言類型的零值。對于指針類型(像*Handler)、slice和map類型,零值為nil,對于結(jié)構(gòu)體類型,零值為空結(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,
) {
  // ...
}

接收者和接口

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

使用 指針類型 接收者的方法只能通過指針或者 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"}}

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

// 值類型調(diào)用Write方法會報編譯錯誤
//  sVals[1].Write("test")

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

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

同樣的,接口可以通過指針調(diào)用,即使這個方法的接收者是指類型。

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}

var i F
i = s1Val
i = s1Ptr
i = s2Ptr

// 這個例子編譯會報錯,因為s2Val是值類型,而S2的方法里接收者是指針類型.
//   i = s2Val

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

Mutexes的零值是有效的

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

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

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

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公開API中不明確說明的一部分。

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

在邊界拷貝Slices和Maps

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

接收 Slices 和 Maps

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

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

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

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

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

// 這樣賦值不會影響 d1.trips(因為 SetTrips 內(nèi)部有copy).
trips[0] = ...

返回 Slices 和 Maps

同樣,請注意用戶對 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鎖保護,任何對 snapshot 的訪問會受數(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)在只是個copy。
snapshot := stats.Snapshot()

使用Defer釋放資源

在讀寫文件、使用鎖時,使用 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 時,很容易漏寫Unlock().
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

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

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

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

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

Bad Good
// 這個 size 對任何操作都夠了!
c := make(chan int, 64)
// Size 是1
c := make(chan int, 1// or
// 無緩沖 channel
c := make(chan int)

枚舉類型值從1開始

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

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)行為時,枚舉類型從0開始是有意義的。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

使用time包來處理時間

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

  1. 一天有24小時
  2. 一小時有60分鐘
  3. 一周有7天
  4. 一年有365天
  5. 其他易錯點

舉例來說, 1 表示在一個時間點加上24小時并不一定會產(chǎn)生新的一天。

因此,在處理時間時應(yīng)始終使用"time"包,因為它會用更安全、準(zhǔn)確的方式來處理這些不正確的假設(shè)。

用 time.Time 表示瞬時時間

需要瞬時時間語義時,使用time.Time ,在進行比較、增加或減少時間段時,使用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 表示時間段

應(yīng)使用 time.Duration 來表示時間段

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

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

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

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

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

對外交互 使用 time.Time 和 time.Duration

在對外交互時盡可能使用 time.Duration 和 time.Time,例如:

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

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

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

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定義的 時間戳格式。默認(rèn)情況下, Time.UnmarshalText 使用這種格式,并可以通過time.RFC3339在Time.Format 和 time.Parse 中使用。

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

錯誤

錯誤類型

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

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

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

無錯誤匹配 有錯誤匹配
// package foo

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

// package bar

if err := foo.Open(); err != nil {
  //無法處理錯誤
  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) {
    // 處理這個錯誤
  } else {
    panic("unknown error")
  }
}

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

無錯誤匹配 有錯誤匹配
// 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)出了錯誤變量或錯誤類型,那這個錯誤將變成你包里公共API的一部分。

錯誤包裝

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

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

返回原始錯誤不會附加上下文信息,這樣就保持了原始錯誤類型和信息。比較適用于lib庫類型代碼 展示底層錯誤信息。

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

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

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

當(dāng)為返回錯誤增加上下文信息時,避免在上下文中增加像 "failed to" 這樣的沒啥用的短語,這樣沒用的短語在錯誤 堆棧中堆積起來的話,反而不利于你定位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)你的錯誤傳給別的系統(tǒng)時,錯誤信息應(yīng)該足夠清晰。(比如, 錯誤信息在日志中以 "Failed" 開頭)

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

錯誤命名

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

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

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

對于自定義錯誤類型,請使用Error后綴。

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

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

// 這個錯誤是非導(dǎo)出類型,不會作為我們公共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ū)別

處理斷言失敗

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

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

不要使用Panic

在生產(chǎn)環(huán)境的業(yè)務(wù)代碼避免使用panic。Panics 是級聯(lián)問題cascading failures的主要來源。如果發(fā)生錯誤,函數(shù)必須返回錯誤,讓調(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 不是錯誤處理策略。當(dāng)系統(tǒng)發(fā)生像空指針異常這種 不可恢復(fù)的 Fatal 異常時,才需要使用Panic。唯一的意外情況是項目啟動:如果程序啟動階段出現(xiàn)問題需要拋出異常。

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

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

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 包的原子操作對數(shù)據(jù)類型進行操作(int32, int64, 等.) 。因為自己操作容易忘記使用原子操作對變量進行讀取和修改。

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

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

避免使用全局可變對象

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)嵌類型

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

假設(shè)你用AbstractList結(jié)構(gòu)體實現(xiàn)了公共的 list 方法,避免在其他實現(xiàn)中內(nèi)嵌AbstractList類型。而是應(yīng)該在其他結(jié)構(gòu)體中顯式聲明list,并在方法實現(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)嵌類型type embedding作為組合和繼承的折中方案。外部的結(jié)構(gòu)體會獲得內(nèi)嵌類型的隱式拷貝。默認(rèn)情況下,內(nèi)嵌類型的方法會嵌入實例的同一方法。

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

很少場景需要嵌入類型,雖然嵌入類型很方便,讓你避免編寫冗長的方法。

即使是用interface而不是結(jié)構(gòu)體來嵌入方法,這是給開發(fā)人員帶來了一定的靈活性,但是仍然暴露了具體實現(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)體還是嵌入接口,都會限制類型的演化。

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

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

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

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

根據(jù)上下文的不同,用預(yù)聲明標(biāo)識符命名變量可能會在當(dāng)前作用域下覆蓋官方標(biāo)識符,讓你的代碼變得難以理解。最好的情況下,編譯器會直接報錯,最糟糕的情況下,這樣的代碼會引入難以排查的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)識符時編譯器不會報錯,但是像 go vet 這樣的工具會告訴你標(biāo)識符被覆蓋的情況。

避免使用init()

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

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

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

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)勢:

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

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

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

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

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
}

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

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

一次性退出

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

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

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)該用指定對應(yīng)項tag標(biāo)簽進行注釋。

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)和字段會破壞這個契約。在結(jié)構(gòu)體中聲明tag 可以防止重構(gòu)結(jié)構(gòu)體中意外違反約定。

性能

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

使用 strconv 而不是 fmt

當(dāng)需要原始類型和字符串互相轉(zhuǎn)化時,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類型,應(yīng)該在for循環(huán)開始前把[]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ù)先指定容器類型的容量

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

指定Map容量

如果有可能,用make來初始化map類型,并指定map的大小。

make(map[T1]T2, hint)

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

注意,和 slice 不同,給map指定容量不意味著搶占式內(nèi)存分配完成,而是會用于預(yù)估的哈希表內(nèi)部 buckets。因此,當(dāng)你給 map 添加元素,或者給 map 指定值時,仍有可能發(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 沒有指定內(nèi)存大小,因此在運行期間可能會有更多的內(nèi)存分配。

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

指定Slice容量

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

make([]T, length, capacity)

和 map 不同,slice的容量不是一個提示:編譯器會根據(jù) make() 提供的容量信息申請足夠的內(nèi)存, 這意味著后續(xù)的 append() 操作不會申請內(nèi)存(除非slice的長度和容量相等,這樣的話后續(xù)添加元素 會申請內(nè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ī)范

避免代碼過長

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

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

一致性

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

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

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

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

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

相似聲明放一組

Go語言支持組引用。

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

組聲明同樣適用于常量、變量和類型聲明。

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)的變量聲明到一個組里,不想管的聲明放在多個組里。

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

  // ...
}

例外:對于變量聲明,尤其是函數(shù)中的變量聲明,不管他們之間是否有關(guān)系,都應(yīng)該被放在一個組內(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)庫
  • 其他庫

默認(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)命名包時,應(yīng)按照下面原則命名:

  • 全小寫字母。無大寫字母或下劃線。
  • 大多數(shù)導(dǎo)入包的情況下,不需要對包重新命名。
  • 簡短而簡潔,因為當(dā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 約定。一種其他情況是使用測試函數(shù)。測試函數(shù) 命名可以包含下劃線以便于相關(guān)測試函數(shù)進行分組。比如:TestMyFunction_WhatIsBeingTested。

導(dǎo)入別名

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

import (
  "net/http"

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

其他情況下,除非幾個包之間有導(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)用順序排序。
  • 一個文件中的函數(shù)應(yīng)該按照接收者分組。

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

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

由于函數(shù)是按照接收者進行分組的,普通的工具函數(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)通過盡早處理錯誤/特殊情況盡早處理/循環(huán)中使用 continue 等手段,來減少嵌套代碼過多問題。

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

沒用的else

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

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

頂層變量聲明

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

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á)式的類型和所需類型不一樣,需要指定類型。

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)出變量使用_前綴

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

原因:頂層聲明的變量作用域一般是包范圍。用一個常見的名字可能會導(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)出的錯誤類型一般使用不帶 _ 的 err 前綴。參考Error Naming.

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

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

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

  version int
}

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

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

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

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

簡單概括,使用嵌入類型時要明確目的。一個不錯的方式是:"這些嵌入的字段/方法是否需要被直接添加到外部 類型",如果答案是"一些"或者"No",不要使用內(nè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
}

本地變量聲明

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

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

然而,有些情況下用 var 會讓聲明語句更加清晰,比如聲明空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 代表長度為0的有效slice, 意味著:

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

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

    Bad Good
    func isEmpty(s []stringbool {
      return s == nil
    }
    
    func isEmpty(s []stringbool {
      return len(s) == 0
    }
    
  • 用 var 聲明的零值slice是有效的,沒必要用 make 來創(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有效,但是它不等于長度為0的 slice。在一些情況下(比如說序列化), 這兩種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ù)語義不明確

在函數(shù)中裸傳參數(shù)值會讓代碼語義不明確,可以添加 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 換成自定義類型。因為未來可能不僅僅局限于兩個bool值(true/false)。

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

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

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

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

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

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

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

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

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

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

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

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

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

當(dāng)初始化結(jié)構(gòu)體字段時,除非需要提供一個有意義的上下文,否則需要忽略對零值字段進行賦值。因為Go 會自動給這些零值字段進行填充。

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

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

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

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

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

當(dāng)結(jié)構(gòu)體中所有的字段都為空時,用 var 來聲明結(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)引用時,請使用&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來創(chuàng)建空map,這樣使得map的初始化不同于聲明,而且你還可以在 make 中添加map的大小提示。

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

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

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

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

另外,如果map初始化的時候需要賦值固定信息,使用 map literals 方式來初始化。

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時增加一組固定的元素,就使用map literals。否則就使用 make(如果可以, 盡可能指定map的容量)。

在Printf外面格式化字符串

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

這有助于go vet對格式字符串執(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ù)時,應(yīng)保證go vet可以檢測到他的格式化字符串。

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

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

go vet -printfuncs=wrapf,statusf

參考 go vet: Printf family check.

Patterns

Test Tables

當(dāng)你的測試用例形式上重復(fù)時,用 subtests 方式編寫case會讓測試用例看起來更加簡潔。

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的方式,在拓展測試用例時也會顯得更加清晰。

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

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

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

對于并行測試,比如一些特殊的循環(huán) (比如那些生產(chǎn) goroutine 或 在循環(huán)中捕獲引用的循環(huán)), 需要 注意在循環(huán)中明確分配循環(huán)變量來確保不會產(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)中聲明一個 tt 變量。如果不這么做,大多數(shù)測試用例都會收到一個非預(yù)期的 tt,或是一個在運行期改變的值。

函數(shù)功能選項API

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

對于需要拓展參數(shù)的構(gòu)造方法或是其他需要可選參數(shù)的公共API可以考慮這種模式,對于參數(shù)在三個及以上 的函數(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)建一個連接
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 只在需要時才被提供。

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

我們建議這種模式的實現(xiàn)方式是 提供一個 Option 接口,里面有一個非導(dǎo)出類型方法,在一個非 導(dǎo)出類型的 options 結(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 創(chuàng)建一個連接
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    cache:  defaultCache,
    logger: zap.NewNop(),
  }

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

  // ...
}

還有一種用閉包實現(xiàn)這種方法的模式,但我們認(rèn)為上面提供的這種模式給作者提供了更高的靈活性,更 易于調(diào)試和測試。這種方式可以在測試和mock中進行比較,而閉包方式難以做到。此外,它允許 option 實現(xiàn)其他接口,比如 fmt.Stringer,會 string 類型的可讀性更高。

還可以參考:

  • Self-referential functions and the design of options

  • Functional options for friendly APIs

Linting

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

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

  • errcheck 確保錯誤被處理

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

  • golint 指出常見的文本錯誤

  • govet 分析代碼中的常見錯誤

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

Lint Runners

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

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


3 人點贊