Uber 公司Golang編程規範【翻譯】

引言

樣式是支配我們代碼的慣例。 術語“樣式”有點用詞不當,因爲這些約定不僅僅涵蓋那些可以由gofmt替我們處理的源文件格式。

本指南的目的是通過詳細描述在Uber編寫Go代碼的注意事項來管理這種複雜性。 這些規則的存在是爲了使代碼庫易於管理,同時仍然允許工程師有效地使用Go語言功能。

該指南最初由Prashant Varanasi和Simon Newton編寫,目的是使一些同事快速使用Go。 多年來,已根據其他人的反饋進行了修改。

本文檔記錄了我們在Uber遵循的Go代碼中的慣用約定。 其中許多是Go的通用準則,而其他準則則依賴於外部資源:

  1. Effective Go
  2. The Go common mistakes guide

通過golintgo vet運行時,所有代碼均應無錯誤。 我們建議您將編輯器設置爲:

準則

Pointers to Interfaces

您幾乎不需要指向接口的指針。 您應該將接口作爲值傳遞-底層數據仍然可以是指針。

一個接口是兩個字段:

  • 指向某些特定類型信息的指針。 您可以將其視爲“類型”。
  • 數據指針。 如果存儲的數據是指針,則直接存儲。 如果存儲的數據是一個值,則存儲指向該值的指針。

如果要接口方法修改基礎數據,則必須使用指針。

接收器和接口

具有值接收器的方法可以在指針和值上調用。
例如

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

// You can only call Read using a value
sVals[1].Read()

// This will not compile:
//  sVals[1].Write("test")

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

// You can call both Read and Write using a pointer
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

// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
//   i = s2Val

Effective Go has a good write up on Pointers vs. Values.

零值互斥是有效的

sync.Mutexsync.RWMutex的零值是有效的,因此不需要指向互斥量的指針。

  • 推薦使用:
var mu sync.Mutex
mu.Lock()
  • 不推薦使用:
mu := new(sync.Mutex)
mu.Lock()

如果通過指針使用結構,則互斥體可以是非指針字段,或者最好直接嵌入到該結構中。

  • 爲專用類型或需要實現Mutex接口的類型嵌入。
type smap struct {
  sync.Mutex

  data map[string]string
}

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

func (m *smap) Get(k string) string {
  m.Lock()
  defer m.Unlock()

  return m.data[k]
}
  • 對於導出的類型,使用專用鎖。
type SMap struct {
  mu sync.Mutex

  data map[string]string
}

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

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

  return m.data[k]
}

在邊界處複製Slices和Maps

Slices和Maps包含指向基礎數據的指針,因此在需要複製它們時要特別注意方案。

作爲參數接收Slices和Maps

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

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

// We can now modify trips[0] without affecting d1.trips.
trips[0] = ...
  • 不推薦
func (d *Driver) SetTrips(trips []Trip) {
  d.trips = trips
}

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

// Did you mean to modify d1.trips?
trips[0] = ...

返回時Slices 和 Maps

同樣,請注意用戶對顯示內部狀態的map或切片的修改。

  • 推薦
type Stats struct {
  sync.Mutex

  counters map[string]int
}

func (s *Stats) Snapshot() map[string]int {
  s.Lock()
  defer s.Unlock()

  result := make(map[string]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// Snapshot is now a copy.
snapshot := stats.Snapshot()
  • 不推薦
type Stats struct {
  sync.Mutex

  counters map[string]int
}

// Snapshot returns the current stats.
func (s *Stats) Snapshot() map[string]int {
  s.Lock()
  defer s.Unlock()

  return s.counters
}

// snapshot is no longer protected by the lock!
snapshot := stats.Snapshot()

用defer處理清理工作

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

  • 推薦
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// more readable
  • 不推薦
p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

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

return newCount

// easy to miss unlocks due to multiple returns

Defer的開銷非常小,只有在您可以證明函數執行時間處於納秒級的程度時,才應避免這樣做。 比起微不足道的成本,使用defer的可讀性是值得的。 對於具有比簡單的內存訪問更多的更大的方法尤其如此,其他方法的計算比defer要重要得多。

Channel的通道大小爲1 或 無緩衝

通道通常應爲1大小或無緩衝。 默認情況下,通道是無緩衝的,大小爲零。 任何其他大小都必須經過嚴格的審查。 考慮如何確定大小,什麼阻塞通道在負載下填滿並阻塞寫入器,以及發生這種情況時會發生什麼。

  • 推薦
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int)
  • 不推薦
// Ought to be enough for anybody!
c := make(chan int, 64)

從1 開始枚舉

在Go中引入枚舉的標準方法是聲明自定義類型和使用iota組合const。 由於變量的默認值爲0,因此通常應以非零值開頭枚舉。

  • 推薦
type Operation int

const (
 Add Operation = iota + 1
 Subtract
 Multiply
)

// Add=1, Subtract=2, Multiply=3
  • 不推薦
type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

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

在某些情況下,使用零值是有意義的,例如,當零值是理想的默認行爲時。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

錯誤類型

有多種聲明錯誤的選項:

  • errors.New對於簡單靜態字符串的錯誤
  • fmt.Errorf用於格式化的錯誤字符串
  • 實現Error()方法的自定義類型
  • 包裝錯誤使用"pkg/errors".Wrap
    返回錯誤時,請考慮以下因素以確定最佳選擇:
  • 這是一個不需要額外信息的簡單錯誤嗎?如果是,errors.New 可以滿足
  • 客戶需要檢測並處理此錯誤嗎? 如果是這樣,則應使用自定義類型,並實現Error()方法。
  • 您是否正在傳播下游函數返回的錯誤?如果是,查看section on error wrapping.
  • 否則, fmt.Errorf就ok。

如果客戶端需要檢測錯誤,並且您已經使用errors創建了一個簡單的錯誤,請使用var。

  • 推薦
// 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")
  }
}
  • 不推薦
// 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")
    }
  }
}

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

  • 推薦
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")
    }
  }
}
  • 不推薦
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")
    }
  }
}

直接導出自定義錯誤類型時要小心,因爲它們已成爲程序包公共API的一部分。 最好公開匹配器功能以檢查錯誤。

// package foo

type errNotFound struct {
  file string
}

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

func IsNotFoundError(err error) bool {
  _, ok := err.(errNotFound)
  return ok
}

func Open(file string) error {
  return errNotFound{file: file}
}

// package bar

if err := foo.Open("foo"); err != nil {
  if foo.IsNotFoundError(err) {
    // handle
  } else {
    panic("unknown error")
  }
}

錯誤包裝

調用失敗時,有三種主要的錯誤傳播方式:

  • 如果沒有要添加的其他上下文,並且您想要維護原始錯誤類型,則返回原始錯誤。
  • 使用pkg / errors增加上下文。包裝以便錯誤消息提供更多上下而且pkg / errors.Cause可用於提取原始錯誤。
  • 如果調用方不需要檢測或處理該特定錯誤情況使用fmt.Errorf
    使用示例:
  • 推薦
s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %s", err)
}
//x: y: new store: the error
  • 不推薦
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

但是,一旦將錯誤發送到另一個系統,就應該清楚該消息是一個錯誤(例如,日誌中的err標記或“Failed”前綴)。

處理類型斷言失敗處理類型斷言失敗

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

  • 推薦使用
t, ok := i.(string)
if !ok {
  // handle the error gracefully
}
  • 不推薦單個返回值:
t := i.(string)

不要使用panic

在生產中運行的代碼必須避免出現panic情況。panic 是級聯失敗的主要根源。 如果發生錯誤,該函數必須返回錯誤,並允許調用方決定如何處理它。

  • 推薦
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)
  }
}
  • 不推薦
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])
}

Panic/recover不是錯誤處理策略。僅當發生不可恢復的事情(例如取消nil引用)時,程序才必須panic。程序初始化是一個例外:程序啓動時應使程序中止的不良情況可能會引起panic

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

即使在測試中,也要優先選擇t.Fatalt.FailNow來代替panic,以確保測試標記爲失敗。

  • 推薦
// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}
  • 不推薦
// func TestFoo(t *testing.T)

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

go.uber.org/atomic

使用sync / atomic包的原子操作對原始類型(int32,int64等)進行操作,因此很容易忘記使用原子操作來讀取或修改變量。

go.uber.org/atomic通過隱藏基礎類型爲這些操作增加了類型安全性。 另外,它包括一個方便的atomic.Bool類型。

  • 推薦
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()
}
  • 不推薦官方:
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!
}

性能

優先使用strconv而不是fmt

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

避免string到[]byte的轉換

不要重複從固定字符串創建字節片。相反,請執行一次轉換並捕獲結果。

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

風格

組相似聲明

  • 推薦
import (
  "a"
  "b"
)

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

type (
  Area float64
  Volume float64
)
  • 不推薦
import "a"
import "b"

const a = 1
const b = 2

var a = 1
var b = 2

type Area float64
type Volume float64

僅與組相關的聲明。 不要對不相關的聲明進行分組。

  • 推薦
type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

const ENV_VAR = "MY_ENV"
  • 不推薦
type Operation int

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

組不受使用位置的限制。
例如,您可以在函數內部使用它們。

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

  ...
}

import 順序

應該有兩個導入組:

  • 標準庫
  • 其他一切
    默認情況下,這是goimports應用的分組。
import (
  "fmt"
  "os"

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

包命名

命名包時,按如下原則選擇:

  • 全部小寫。沒有大寫或下劃線
  • 大多數情況不需要在導入時重命名
  • 簡短而簡潔。請記住,在每個調用站點上都完整標識了該名稱。
  • 不復數。例如,net / url,而不是net / url。
  • 不用commonutilsharedlib。這些是不好的,無用的名稱。

可以參考 Package Namesstyle guideline for Go packages.

函數命名

導入別名

函數分組排序

  • 函數應按粗略的調用順序排序。
  • 文件中的函數應按接收者分組。
    因此,導出的函數應首先出現在文件中,然後是structconstvar定義。

減少嵌套

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

不必要的else

頂級變量聲明

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

使用_前綴未導出的全局變量

嵌入結構

type Client struct {
  http.Client

  version int
}

使用字段名稱初始化結構

## suggest
k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}
## no suggest
k := User{"John", "Doe", true}
  • 例外: 如果字段數少於或等於3,則測試表中的字段名可以省略。

局部變量聲明

如果將變量顯式設置爲某個值,則應使用短變量聲明:=

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

nil是有效切片

nil是長度爲0的有效切片。

縮小變量範圍

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

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

初始化結構引用

初始化結構引用時,請使用&T{}而不是new(T),以使其與結構初始化一致。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章