1. 程式人生 > 其它 >Go語言開發規範

Go語言開發規範

Go 語言編碼規範參看 Uber

Uber 是一家美國矽谷的科技公司,也是 Go 語言的早期 adopter。其開源了很多 golang 專案,諸如被 Gopher 圈熟知的 zapjaeger 等。2018 年年末 Uber 將內部的 Go 風格規範 開源到 GitHub,經過一年的積累和更新,該規範已經初具規模,並受到廣大 Gopher 的關注。本文是該規範的中文版本。本版本會根據原版實時更新。

版本

  • 當前更新版本:2021-04-23 版本地址:commit:#114
  • 如果您發現任何更新、問題或改進,請隨時 fork 和 PR
  • Please feel free to fork and PR if you find any updates, issues or improvement.

目錄

介紹

樣式 (style) 是支配我們程式碼的慣例。術語樣式有點用詞不當,因為這些約定涵蓋的範圍不限於由 gofmt 替我們處理的原始檔格式。

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

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

本文件記錄了我們在 Uber 遵循的 Go 程式碼中的慣用約定。其中許多是 Go 的通用準則,而其他擴充套件準則依賴於下面外部的指南:

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

所有程式碼都應該通過golintgo vet的檢查並無錯誤。我們建議您將編輯器設定為:

  • 儲存時執行 goimports
  • 執行 golintgo vet 檢查錯誤

您可以在以下 Go 編輯器工具支援頁面中找到更為詳細的資訊:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins

指導原則

指向 interface 的指標

您幾乎不需要指向介面型別的指標。您應該將介面作為值進行傳遞,在這樣的傳遞過程中,實質上傳遞的底層資料仍然可以是指標。

介面實質上在底層用兩個欄位表示:

  1. 一個指向某些特定型別資訊的指標。您可以將其視為"type"。
  2. 資料指標。如果儲存的資料是指標,則直接儲存。如果儲存的資料是一個值,則儲存指向該值的指標。

如果希望介面方法修改基礎資料,則必須使用指標傳遞(將物件指標賦值給介面變數)。

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

// f1.f()無法修改底層資料
// f2.f() 可以修改底層資料,給介面變數f2賦值時使用的是物件指標
var f1 F = S1{}
var f2 F = &S2{}

Interface 合理性驗證

在編譯時驗證介面的符合性。這包括:

  • 將實現特定介面的匯出型別作為介面API 的一部分進行檢查
  • 實現同一介面的(匯出和非匯出)型別屬於實現型別的集合
  • 任何違反介面合理性檢查的場景,都會終止編譯,並通知給使用者

補充:上面3條是編譯器對介面的檢查機制,
大體意思是錯誤使用介面會在編譯期報錯.
所以可以利用這個機制讓部分問題在編譯期暴露.

BadGood
// 如果Handler沒有實現http.Handler,會在執行時報錯
type Handler struct {
  // ...
}
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}
type Handler struct {
  // ...
}
// 用於觸發編譯期的介面的合理性檢查機制
// 如果Handler沒有實現http.Handler,會在編譯期報錯
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

如果 *Handlerhttp.Handler 的介面不匹配,
那麼語句 var _ http.Handler = (*Handler)(nil) 將無法編譯通過.

賦值的右邊應該是斷言型別的零值。
對於指標型別(如 *Handler)、切片和對映,這是 nil
對於結構型別,這是空結構。

type LogHandler struct {
  h   http.Handler
  log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

接收器 (receiver) 與介面

使用值接收器的方法既可以通過值呼叫,也可以通過指標呼叫。

帶指標接收器的方法只能通過指標或 addressable values呼叫.

例如,

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

// 你只能通過值呼叫 Read
sVals[1].Read()

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

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

// 通過指標既可以呼叫 Read,也可以呼叫 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")

類似的,即使方法有了值接收器,也同樣可以用指標接收器來滿足介面.

type F interface {
  f()
}

type S1 struct{}

func (s S1) f() {}

type S2 struct{}

func (s *S2) f() {}

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

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

//  下面程式碼無法通過編譯。因為 s2Val 是一個值,而 S2 的 f 方法中沒有使用值接收器
//   i = s2Val

Effective Go 中有一段關於 pointers vs. values 的精彩講解。

補充:

  • 一個型別可以有值接收器方法集和指標接收器方法集
    • 值接收器方法集是指標接收器方法集的子集,反之不是
  • 規則
    • 值物件只可以使用值接收器方法集
    • 指標物件可以使用 值接收器方法集 + 指標接收器方法集
  • 介面的匹配(或者叫實現)
    • 型別實現了介面的所有方法,叫匹配
    • 具體的講,要麼是型別的值方法集匹配介面,要麼是指標方法集匹配介面

具體的匹配分兩種:

  • 值方法集和介面匹配
    • 給介面變數賦值的不管是值還是指標物件,都ok,因為都包含值方法集
  • 指標方法集和介面匹配
    • 只能將指標物件賦值給介面變數,因為只有指標方法集和介面匹配
    • 如果將值物件賦值給介面變數,會在編譯期報錯(會觸發介面合理性檢查機制)

為啥 i = s2Val 會報錯,因為值方法集和介面不匹配.

零值 Mutex 是有效的

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

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

如果你使用結構體指標,mutex 可以非指標形式作為結構體的組成欄位,或者更好的方式是直接嵌入到結構體中。
如果是私有結構體型別或是要實現 Mutex 介面的型別,我們可以使用嵌入 mutex 的方法:

type smap struct {
  sync.Mutex // only for unexported types(僅適用於非匯出型別)

  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

請記住,當 map 或 slice 作為函式引數傳入時,如果您儲存了對它們的引用,則使用者可以對其進行修改。

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)

// 這裡我們修改 trips[0],但不會影響到 d1.trips
trips[0] = ...

返回 slices 或 maps

同樣,請注意使用者對暴露內部狀態的 map 或 slice 的修改。

BadGood
type Stats struct {
  mu sync.Mutex

  counters map[string]int
}

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

  return s.counters
}

// snapshot 不再受互斥鎖保護
// 因此對 snapshot 的任何訪問都將受到資料競爭的影響
// 影響 stats.counters
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]int, len(s.counters))
  for k, v := range s.counters {
    result[k] = v
  }
  return result
}

// snapshot 現在是一個拷貝
snapshot := stats.Snapshot()

使用 defer 釋放資源

使用 defer 釋放資源,諸如檔案和鎖。

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

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

return newCount

// 當有多個 return 分支時,很容易遺忘 unlock
p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// 更可讀

Defer 的開銷非常小,只有在您可以證明函式執行時間處於納秒級的程度時,才應避免這樣做。使用 defer 提升可讀性是值得的,因為使用它們的成本微不足道。尤其適用於那些不僅僅是簡單記憶體訪問的較大的方法,在這些方法中其他計算的資源消耗遠超過 defer

Channel 的 size 要麼是 1,要麼是無緩衝的

channel 通常 size 應為 1 或是無緩衝的。預設情況下,channel 是無緩衝的,其 size 為零。任何其他尺寸都必須經過嚴格的審查。我們需要考慮如何確定大小,考慮是什麼阻止了 channel 在高負載下和阻塞寫時的寫入,以及當這種情況發生時系統邏輯有哪些變化。(翻譯解釋:按照原文意思是需要界定通道邊界,競態條件,以及邏輯上下文梳理)

BadGood
// 應該足以滿足任何情況!
c := make(chan int, 64)
// 大小:1
c := make(chan int, 1) // 或者
// 無緩衝 channel,大小為 0
c := make(chan int)

列舉從 1 開始

在 Go 中引入列舉的標準方法是宣告一個自定義型別和一個使用了 iota 的 const 組。由於變數的預設值為 0,因此通常應以非零值開頭列舉。

BadGood
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

在某些情況下,使用零值是有意義的(列舉從零開始),例如,當零值是理想的預設行為時。

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

使用 time 處理時間

時間處理很複雜。關於時間的錯誤假設通常包括以下幾點。

  1. 一天有 24 小時
  2. 一小時有 60 分鐘
  3. 一週有七天
  4. 一年 365 天
  5. 還有更多

例如,1 表示在一個時間點上加上 24 小時並不總是產生一個新的日曆日。

因此,在處理時間時始終使用 "time" 包,因為它有助於以更安全、更準確的方式處理這些不正確的假設。

使用 time.Time 表達瞬時時間

在處理時間的瞬間時使用 time.Time,在比較、新增或減去時間時使用 time.Time 中的方法。

BadGood
func isActive(now, start, stop int) bool {
  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 表達時間段

在處理時間段時使用 time.Duration .

BadGood
func poll(delay int) {
  for {
    // ...
    time.Sleep(time.Duration(delay) * time.Millisecond)
  }
}
poll(10) // 是幾秒鐘還是幾毫秒?
func poll(delay time.Duration) {
  for {
    // ...
    time.Sleep(delay)
  }
}
poll(10*time.Second)

回到第一個例子,在一個時間瞬間加上 24 小時,我們用於新增時間的方法取決於意圖。如果我們想要下一個日曆日(當前天的下一天)的同一個時間點,我們應該使用 Time.AddDate。但是,如果我們想保證某一時刻比前一時刻晚 24 小時,我們應該使用 Time.Add

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

對外部系統使用 time.Timetime.Duration

儘可能在與外部系統的互動中使用 time.Durationtime.Time 例如 :

當不能在這些互動中使用 time.Duration 時,請使用 intfloat64,並在欄位名稱中包含單位。

例如,由於 encoding/json 不支援 time.Duration,因此該單位包含在欄位的名稱中。

BadGood
// {"interval": 2}
type Config struct {
  Interval int `json:"interval"`
}
// {"intervalMillis": 2000}
type Config struct {
  IntervalMillis int `json:"intervalMillis"`
}

當在這些互動中不能使用 time.Time 時,除非達成一致,否則使用 stringRFC 3339 中定義的格式時間戳。預設情況下,Time.UnmarshalText 使用此格式,並可通過 time.RFC3339Time.Formattime.Parse 中使用。

儘管這在實踐中並不成問題,但請記住,"time" 包不支援解析閏秒時間戳(8728),也不在計算中考慮閏秒(15190)。如果您比較兩個時間瞬間,則差異將不包括這兩個瞬間之間可能發生的閏秒。

錯誤型別

Go 中有多種宣告錯誤(Error) 的選項:

返回錯誤時,請考慮以下因素以確定最佳選擇:

  • 這是一個不需要額外資訊的簡單錯誤嗎?如果是這樣,errors.New 足夠了。

  • 客戶需要檢測並處理此錯誤嗎?如果是這樣,則應使用自定義型別並實現該 Error() 方法。

  • 您是否正在傳播下游函式返回的錯誤?如果是這樣,請檢視本文後面有關錯誤包裝 section on error wrapping 部分的內容。

  • 否則 fmt.Errorf 就可以了。

如果客戶端需要檢測錯誤,並且您已使用建立了一個簡單的錯誤 errors.New,請使用一個錯誤變數。

BadGood
// 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")
    }
  }
}
// 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) {
    // handle
  } else {
    panic("unknown error")
  }
}

如果您有可能需要客戶端檢測的錯誤,並且想向其中新增更多資訊(例如,它不是靜態字串),則應使用自定義型別。

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

func use() {
  if err := open("testfile.txt"); err != nil {
    if strings.Contains(err.Error(), "not found") {
      // 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("testfile.txt"); err != nil {
    if _, ok := err.(errNotFound); ok {
      // 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")
  }
}

錯誤包裝 (Error Wrapping)

一個(函式/方法)呼叫失敗時,有三種主要的錯誤傳播方式:

  • 如果沒有要新增的其他上下文,並且您想要維護原始錯誤型別,則返回原始錯誤。
  • 新增上下文,使用 "pkg/errors".Wrap 以便錯誤訊息提供更多上下文 ,"pkg/errors".Cause 可用於提取原始錯誤。
  • 如果呼叫者不需要檢測或處理的特定錯誤情況,使用 fmt.Errorf

建議在可能的地方新增上下文,以使您獲得諸如“呼叫服務 foo:連線被拒絕”之類的更有用的錯誤,而不是諸如“連線被拒絕”之類的模糊錯誤。

在將上下文新增到返回的錯誤時,請避免使用“failed to”之類的短語以保持上下文簡潔,這些短語會陳述明顯的內容,並隨著錯誤在堆疊中的滲透而逐漸堆積:

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

但是,一旦將錯誤傳送到另一個系統,就應該明確訊息是錯誤訊息(例如使用err標記,或在日誌中以”Failed”為字首)。

另請參見 Don't just check errors, handle them gracefully. 不要只是檢查錯誤,要優雅地處理錯誤

處理型別斷言失敗

type assertion 的單個返回值形式針對不正確的型別將產生 panic。因此,請始終使用“comma ok”的慣用法。

BadGood
t := i.(string)
t, ok := i.(string)
if !ok {
  // 優雅地處理錯誤
}

不要 panic

在生產環境中執行的程式碼必須避免出現 panic。panic 是 cascading failures 級聯失敗的主要根源 。如果發生錯誤,該函式必須返回錯誤,並允許呼叫方決定如何處理它。

BadGood
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 不是錯誤處理策略。僅當發生不可恢復的事情(例如:nil 引用)時,程式才必須 panic。程式初始化是一個例外:程式啟動時應使程式中止的不良情況可能會引起 panic。

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

即使在測試程式碼中,也優先使用t.Fatal或者t.FailNow而不是 panic 來確保失敗被標記。

BadGood
// func TestFoo(t *testing.T)

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

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

使用 go.uber.org/atomic

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

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

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

避免可變全域性變數

使用選擇依賴注入方式避免改變全域性變數。
既適用於函式指標又適用於其他值型別

BadGood
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  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 string) string {
  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))
}

避免在公共結構中嵌入型別

這些嵌入的型別洩漏實現細節、禁止型別演化和模糊的文件。

假設您使用共享的 AbstractList 實現了多種列表型別,請避免在具體的列表實現中嵌入 AbstractList
相反,只需手動將方法寫入具體的列表,該列表將委託給抽象列表。

type AbstractList struct {}
// 新增將實體新增到列表中。
func (l *AbstractList) Add(e Entity) {
  // ...
}
// 移除從列表中移除實體。
func (l *AbstractList) Remove(e Entity) {
  // ...
}
BadGood
// ConcreteList 是一個實體列表。
type ConcreteList struct {
  *AbstractList
}
// ConcreteList 是一個實體列表。
type ConcreteList struct {
  list *AbstractList
}
// 新增將實體新增到列表中。
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}
// 移除從列表中移除實體。
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

Go 允許 型別嵌入 作為繼承和組合之間的折衷。
外部型別獲取嵌入型別的方法的隱式副本。
預設情況下,這些方法委託給嵌入例項的同一方法。

結構還獲得與型別同名的欄位。
所以,如果嵌入的型別是 public,那麼欄位是 public。為了保持向後相容性,外部型別的每個未來版本都必須保留嵌入型別。

很少需要嵌入型別。
這是一種方便,可以幫助您避免編寫冗長的委託方法。

即使嵌入相容的抽象列表 interface,而不是結構體,這將為開發人員提供更大的靈活性來改變未來,但仍然洩露了具體列表使用抽象實現的細節。

BadGood
// AbstractList 是各種實體列表的通用實現。
type AbstractList interface {
  Add(Entity)
  Remove(Entity)
}
// ConcreteList 是一個實體列表。
type ConcreteList struct {
  AbstractList
}
// ConcreteList 是一個實體列表。
type ConcreteList struct {
  list AbstractList
}
// 新增將實體新增到列表中。
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}
// 移除從列表中移除實體。
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

無論是使用嵌入式結構還是使用嵌入式介面,嵌入式型別都會限制類型的演化.

  • 向嵌入式介面新增方法是一個破壞性的改變。
  • 刪除嵌入型別是一個破壞性的改變。
  • 即使使用滿足相同介面的替代方法替換嵌入型別,也是一個破壞性的改變。

儘管編寫這些委託方法是乏味的,但是額外的工作隱藏了實現細節,留下了更多的更改機會,還消除了在文件中發現完整列表介面的間接性操作。

避免使用內建名稱

Go語言規範language specification 概述了幾個內建的,
不應在Go專案中使用的名稱標識predeclared identifiers

根據上下文的不同,將這些識別符號作為名稱重複使用,
將在當前作用域(或任何巢狀作用域)中隱藏原始識別符號,或者混淆程式碼。
在最好的情況下,編譯器會報錯;在最壞的情況下,這樣的程式碼可能會引入潛在的、難以恢復的錯誤。

BadGood
var error string
// `error` 作用域隱式覆蓋

// or

func handleErrorMessage(error string) {
    // `error` 作用域隱式覆蓋
}
var errorMessage string
// `error` 指向內建的非覆蓋

// or

func handleErrorMessage(msg string) {
    // `error` 指向內建的非覆蓋
}
type Foo struct {
    // 雖然這些欄位在技術上不構成陰影,但`error`或`string`字串的重對映現在是不明確的。
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` 和 `f.error` 在視覺上是相似的
    return f.error
}

func (f Foo) String() string {
    // `string` and `f.string` 在視覺上是相似的
    return f.string
}
type Foo struct {
    // `error` and `string` 現在是明確的。
    err error
    str string
}

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

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

注意,編譯器在使用預先分隔的識別符號時不會生成錯誤,
但是諸如go vet之類的工具會正確地指出這些和其他情況下的隱式問題。

避免使用 init()

儘可能避免使用init()。當init()是不可避免或可取的,程式碼應先嚐試:

  1. 無論程式環境或呼叫如何,都要完全確定。
  2. 避免依賴於其他init()函式的順序或副作用。雖然init()順序是明確的,但程式碼可以更改,
    因此init()函式之間的關係可能會使程式碼變得脆弱和容易出錯。
  3. 避免訪問或操作全域性或環境狀態,如機器資訊、環境變數、工作目錄、程式引數/輸入等。
  4. 避免I/O,包括檔案系統、網路和系統呼叫。

不能滿足這些要求的程式碼可能屬於要作為main()呼叫的一部分(或程式生命週期中的其他地方),
或者作為main()本身的一部分寫入。特別是,打算由其他程式使用的庫應該特別注意完全確定性,
而不是執行“init magic”

BadGood
type Foo struct {
    // ...
}
var _defaultFoo Foo
func init() {
    _defaultFoo = Foo{
        // ...
    }
}
var _defaultFoo = Foo{
    // ...
}
// or, 為了更好的可測試性:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}
type Config struct {
    // ...
}
var _config Config
func init() {
    // Bad: 基於當前目錄
    cwd, _ := os.Getwd()
    // Bad: I/O
    raw, _ := ioutil.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 := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // handle err
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

考慮到上述情況,在某些情況下,init()可能更可取或是必要的,可能包括:

  • 不能表示為單個賦值的複雜表示式。

  • 可插入的鉤子,如database/sql、編碼型別登錄檔等。

  • Google Cloud Functions和其他形式的確定性預計算的優化。

追加時優先指定切片容量

追加時優先指定切片容量

在儘可能的情況下,在初始化要追加的切片時為make()提供一個容量值。

BadGood
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

主函式退出方式(Exit)

Go程式使用os.Exit 或者 log.Fatal* 立即退出 (使用panic不是退出程式的好方法,請 don't panic.)

僅在main()中呼叫其中一個 os.Exit 或者 log.Fatal*。所有其他函式應將錯誤返回到訊號失敗中。

BadGood
func main() {
  body := readFile(path)
  fmt.Println(body)
}
func readFile(path string) string {
  f, err := os.Open(path)
  if err != nil {
    log.Fatal(err)
  }
  b, err := ioutil.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 := ioutil.ReadAll(f)
  if err != nil {
    return "", err
  }
  return string(b), nil
}

原則上:退出的具有多種功能的程式存在一些問題:

  • 不明顯的控制流:任何函式都可以退出程式,因此很難對控制流進行推理。
  • 難以測試:退出程式的函式也將退出呼叫它的測試。這使得函式很難測試,並引入了跳過 go test 尚未執行的其他測試的風險。
  • 跳過清理:當函式退出程式時,會跳過已經進入defer佇列裡的函式呼叫。這增加了跳過重要清理任務的風險。

一次性退出

如果可能的話,你的main()函式中最多一次 呼叫 os.Exit或者log.Fatal。如果有多個錯誤場景停止程式執行,請將該邏輯放在單獨的函式下並從中返回錯誤。
這會縮短 main()函式,並將所有關鍵業務邏輯放入一個單獨的、可測試的函式中。

BadGood
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()
  // 如果我們呼叫log.Fatal 在這條線之後
  // f.Close 將會被執行.
  b, err := ioutil.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 := ioutil.ReadAll(f)
  if err != nil {
    return err
  }
  // ...
}

效能

效能方面的特定準則只適用於高頻場景。

優先使用 strconv 而不是 fmt

將原語轉換為字串或從字串轉換時,strconv速度比fmt快。

BadGood
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

避免字串到位元組的轉換

不要反覆從固定字串建立位元組 slice。相反,請執行一次轉換並捕獲結果。

BadGood
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

指定容器容量

儘可能指定容器容量,以便為容器預先分配記憶體。這將在新增元素時最小化後續分配(通過複製和調整容器大小)。

指定Map容量提示

在儘可能的情況下,在使用 make() 初始化的時候提供容量資訊

make(map[T1]T2, hint)

make()提供容量提示會在初始化時嘗試調整map的大小,這將減少在將元素新增到map時為map重新分配記憶體。

注意,與slices不同。map capacity提示並不保證完全的搶佔式分配,而是用於估計所需的hashmap bucket的數量。
因此,在將元素新增到map時,甚至在指定map容量時,仍可能發生分配。

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

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

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

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

m 是在沒有大小提示的情況下建立的; 在執行時可能會有更多分配。

m 是有大小提示建立的;在執行時可能會有更少的分配。

指定切片容量

在儘可能的情況下,在使用make()初始化切片時提供容量資訊,特別是在追加切片時。

make([]T, length, capacity)

與maps不同,slice capacity不是一個提示:編譯器將為提供給make()的slice的容量分配足夠的記憶體,
這意味著後續的append()`操作將導致零分配(直到slice的長度與容量匹配,在此之後,任何append都可能調整大小以容納其他元素)。

BadGood
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

規範

一致性

本文中概述的一些標準都是客觀性的評估,是根據場景、上下文、或者主觀性的判斷;

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

一致性的程式碼更容易維護、是更合理的、需要更少的學習成本、並且隨著新的約定出現或者出現錯誤後更容易遷移、更新、修復 bug

相反,在一個程式碼庫中包含多個完全不同或衝突的程式碼風格會導致維護成本開銷、不確定性和認知偏差。所有這些都會直接導致速度降低、程式碼審查痛苦、而且增加 bug 數量。

將這些標準應用於程式碼庫時,建議在 package(或更大)級別進行更改,子包級別的應用程式通過將多個樣式引入到同一程式碼中,違反了上述關注點。

相似的宣告放在一組

Go 語言支援將相似的宣告放在一個組內。

BadGood
import "a"
import "b"
import (
  "a"
  "b"
)

這同樣適用於常量、變數和型別宣告:

BadGood

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
)

僅將相關的宣告放在一組。不要將不相關的宣告放在一組。

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

分組使用的位置沒有限制,例如:你可以在函式內部使用它們:

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

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

  ...
}

import 分組

匯入應該分為兩組:

  • 標準庫
  • 其他庫

預設情況下,這是 goimports 應用的分組。

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

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

包名

當命名包時,請按下面規則選擇一個名稱:

  • 全部小寫。沒有大寫或下劃線。
  • 大多數使用命名匯入的情況下,不需要重新命名。
  • 簡短而簡潔。請記住,在每個使用的地方都完整標識了該名稱。
  • 不用複數。例如net/url,而不是net/urls
  • 不要用“common”,“util”,“shared”或“lib”。這些是不好的,資訊量不足的名稱。

另請參閱 Package NamesGo 包樣式指南.

函式名

我們遵循 Go 社群關於使用 MixedCaps 作為函式名 的約定。有一個例外,為了對相關的測試用例進行分組,函式名可能包含下劃線,如:TestMyFunction_WhatIsBeingTested.

匯入別名

如果程式包名稱與匯入路徑的最後一個元素不匹配,則必須使用匯入別名。

import (
  "net/http"

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

在所有其他情況下,除非匯入之間有直接衝突,否則應避免匯入別名。

BadGood
import (
  "fmt"
  "os"

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

  nettrace "golang.net/x/trace"
)

函式分組與順序

  • 函式應按粗略的呼叫順序排序。
  • 同一檔案中的函式應按接收者分組。

因此,匯出的函式應先出現在檔案中,放在struct, const, var定義的後面。

在定義型別之後,但在接收者的其餘方法之前,可能會出現一個 newXYZ()/NewXYZ()

由於函式是按接收者分組的,因此普通工具函式應在檔案末尾出現。

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

type something struct{ ... }

func calcCost(n []int) int {...}

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

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

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

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

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

func calcCost(n []int) int {...}

減少巢狀

程式碼應通過儘可能先處理錯誤情況/特殊情況並儘早返回或繼續迴圈來減少巢狀。減少巢狀多個級別的程式碼的程式碼量。

BadGood
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

如果在 if 的兩個分支中都設定了變數,則可以將其替換為單個 if。

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

頂層變數宣告

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

BadGood
var _s string = F()

func F() string { return "A" }
var _s = F()
// 由於 F 已經明確了返回一個字串型別,因此我們沒有必要顯式指定_s 的型別
// 還是那種型別

func F() string { return "A" }

如果表示式的型別與所需的型別不完全匹配,請指定型別。

type myError struct{}

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

func F() myError { return myError{} }

var _e error = F()
// F 返回一個 myError 型別的例項,但是我們要 error 型別

對於未匯出的頂層常量和變數,使用_作為字首

在未匯出的頂級varsconsts, 前面加上字首_,以使它們在使用時明確表示它們是全域性符號。

例外:未匯出的錯誤值,應以err開頭。

基本依據:頂級變數和常量具有包範圍作用域。使用通用名稱可能很容易在其他檔案中意外使用錯誤的值。

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

結構體中的嵌入

嵌入式型別(例如 mutex)應位於結構體內的欄位列表的頂部,並且必須有一個空行將嵌入式欄位與常規欄位分隔開。

BadGood
type Client struct {
  version int
  http.Client
}
type Client struct {
  http.Client

  version int
}

內嵌應該提供切實的好處,比如以語義上合適的方式新增或增強功能。
它應該在對使用者不利影響的情況下完成這項工作(另請參見:避免在公共結構中嵌入型別Avoid Embedding Types in Public Structs)。

嵌入 不應該:

  • 純粹是為了美觀或方便。
  • 使外部型別更難構造或使用。
  • 影響外部型別的零值。如果外部型別有一個有用的零值,則在嵌入內部型別之後應該仍然有一個有用的零值。
  • 作為嵌入內部型別的副作用,從外部型別公開不相關的函式或欄位。
  • 公開未匯出的型別。
  • 影響外部型別的複製形式。
  • 更改外部型別的API或型別語義。
  • 嵌入內部型別的非規範形式。
  • 公開外部型別的實現詳細資訊。
  • 允許使用者觀察或控制型別內部。
  • 通過包裝的方式改變內部函式的一般行為,這種包裝方式會給使用者帶來一些意料之外情況。

簡單地說,有意識地和有目的地嵌入。一種很好的測試體驗是,
"是否所有這些匯出的內部方法/欄位都將直接新增到外部型別"
如果答案是someno,不要嵌入內部型別-而是使用欄位。

BadGood
type A struct {
    // Bad: A.Lock() and A.Unlock() 現在可用
    // 不提供任何功能性好處,並允許使用者控制有關A的內部細節。
    sync.Mutex
}
type countingWriteCloser struct {
    // Good: Write() 在外層提供用於特定目的,
    // 並且委託工作到內部型別的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: 指標更改零值的有用性
    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: 有用的零值
    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
}

使用欄位名初始化結構體

初始化結構體時,應該指定欄位名稱。現在由 go vet 強制執行。

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

例外:如果有 3 個或更少的欄位,則可以在測試表中省略欄位名稱。

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

本地變數宣告

如果將變數明確設定為某個值,則應使用短變數宣告形式 (:=)。

BadGood
var s = "foo"
s := "foo"

但是,在某些情況下,var 使用關鍵字時預設值會更清晰。例如,宣告空切片。

BadGood
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,這意味著,

  • 您不應明確返回長度為零的切片。應該返回nil 來代替。

    BadGood
    if x == "" {
      return []int{}
    }
    
    if x == "" {
      return nil
    }
    
  • 要檢查切片是否為空,請始終使用len(s) == 0。而非 nil

    BadGood
    func isEmpty(s []string) bool {
      return s == nil
    }
    
    func isEmpty(s []string) bool {
      return len(s) == 0
    }
    
  • 零值切片(用var宣告的切片)可立即使用,無需呼叫make()建立。

    BadGood
    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切片是有效的切片,但它不等於長度為0的切片(一個為nil,另一個不是),並且在不同的情況下(例如序列化),這兩個切片的處理方式可能不同。

縮小變數作用域

如果有可能,儘量縮小變數作用範圍。除非它與 減少巢狀的規則衝突。

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

如果需要在 if 之外使用函式呼叫的結果,則不應嘗試縮小範圍。

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

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

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

fmt.Println(cfg)
return nil

避免參數語義不明確(Avoid Naked Parameters)

函式呼叫中的意義不明確的引數可能會損害可讀性。當引數名稱的含義不明顯時,請為引數新增 C 樣式註釋 (/* ... */)

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

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

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

對於上面的示例程式碼,還有一種更好的處理方式是將上面的 bool 型別換成自定義型別。將來,該引數可以支援不僅僅侷限於兩個狀態(true/false)。

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // Maybe we will have a StatusInProgress in the future.
)

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

使用原始字串字面值,避免轉義

Go 支援使用 原始字串字面值,也就是 " ` " 來表示原生字串,在需要轉義的場景下,我們應該儘量使用這種方案來替換。

可以跨越多行幷包含引號。使用這些字串可以避免更難閱讀的手工轉義的字串。

BadGood
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

初始化結構體

使用欄位名初始化結構

初始化結構時,幾乎應該始終指定欄位名。目前由go vet強制執行。

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

例外:當有3個或更少的欄位時,測試表中的欄位名may可以省略。

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

省略結構中的零值欄位

初始化具有欄位名的結構時,除非提供有意義的上下文,否則忽略值為零的欄位。
也就是,讓我們自動將這些設定為零值

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

這有助於通過省略該上下文中的預設值來減少閱讀的障礙。只指定有意義的值。

在欄位名提供有意義上下文的地方包含零值。例如,表驅動測試 中的測試用例可以受益於欄位的名稱,即使它們是零值的。

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

對零值結構使用 var

如果在宣告中省略了結構的所有欄位,請使用 var 宣告結構。

BadGood
user := User{}
var user User

這將零值結構與那些具有類似於為[初始化 Maps]建立的,區別於非零值欄位的結構區分開來,
並與我們更喜歡的declare empty slices方式相匹配。

初始化 Struct 引用

在初始化結構引用時,請使用&T{}代替new(T),以使其與結構體初始化一致。

BadGood
sval := T{Name: "foo"}

// inconsistent
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

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

初始化 Maps

對於空 map 請使用 make(..) 初始化, 並且 map 是通過程式設計方式填充的。
這使得 map 初始化在表現上不同於宣告,並且它還可以方便地在 make 後新增大小提示。

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

宣告和初始化看起來非常相似的。

宣告和初始化看起來差別非常大。

在儘可能的情況下,請在初始化時提供 map 容量大小,詳細請看 指定Map容量提示

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

BadGood
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 初始化列表 來新增一組固定的元素。否則使用 make (如果可以,請儘量指定 map 容量)。

字串 string format

如果你在函式外宣告Printf-style 函式的格式字串,請將其設定為const常量。

這有助於go vet對格式字串執行靜態分析。

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

命名 Printf 樣式的函式

宣告Printf-style 函式時,請確保go vet可以檢測到它並檢查格式字串。

這意味著您應儘可能使用預定義的Printf-style 函式名稱。go vet將預設檢查這些。有關更多資訊,請參見 Printf 系列

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

$ go vet -printfuncs=wrapf,statusf

另請參閱 go vet: Printf family check.

程式設計模式

表驅動測試

當測試邏輯是重複的時候,通過 subtests 使用 table 驅動的方式編寫 case 程式碼看上去會更簡潔。

BadGood
// 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 的方式在程式碼邏輯擴充套件的時候,比如新增 test case,都會顯得更加的清晰。

我們遵循這樣的約定:將結構體切片稱為tests。 每個測試用例稱為tt。此外,我們鼓勵使用givewant字首說明每個測試用例的輸入和輸出值。

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

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

功能選項

功能選項是一種模式,您可以在其中宣告一個不透明 Option 型別,該型別在某些內部結構中記錄資訊。您接受這些選項的可變編號,並根據內部結構上的選項記錄的全部資訊採取行動。

將此模式用於您需要擴充套件的建構函式和其他公共 API 中的可選引數,尤其是在這些功能上已經具有三個或更多引數的情況下。

BadGood
// 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 creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  // ...
}

必須始終提供快取和記錄器引數,即使使用者希望使用預設值。

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)

只有在需要時才提供選項。

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

Our suggested way of implementing this pattern is with an Option interface
that holds an unexported method, recording options on an unexported options
struct.

我們建議實現此模式的方法是使用一個 Option 介面,該介面儲存一個未匯出的方法,在一個未匯出的 options 結構上記錄選項。

type options struct {
  cache  bool
  logger *zap.Logger
}

type Option interface {
  apply(*options)
}

type cacheOption bool

func (c cacheOption) apply(opts *options) {
  opts.cache = bool(c)
}

func WithCache(c bool) Option {
  return cacheOption(c)
}

type loggerOption struct {
  Log *zap.Logger
}

func (l loggerOption) apply(opts *options) {
  opts.logger = l.Log
}

func WithLogger(log *zap.Logger) Option {
  return loggerOption{Log: log}
}

// Open creates a connection.
func Open(
  addr string,
  opts ...Option,
) (*Connection, error) {
  options := options{
    cache:  defaultCache,
    logger: zap.NewNop(),
  }

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

  // ...
}

注意: 還有一種使用閉包實現這個模式的方法,但是我們相信上面的模式為作者提供了更多的靈活性,並且更容易對使用者進行除錯和測試。特別是,在不可能進行比較的情況下它允許在測試和模擬中對選項進行比較。此外,它還允許選項實現其他介面,包括 fmt.Stringer,允許使用者讀取選項的字串表示形式。

還可以參考下面資料:

Linting

比任何 "blessed" linter 集更重要的是,lint在一個程式碼庫中始終保持一致。

我們建議至少使用以下linters,因為我認為它們有助於發現最常見的問題,並在不需要規定的情況下為程式碼質量建立一個高標準:

  • errcheck 以確保錯誤得到處理

  • goimports 格式化程式碼和管理 imports

  • golint 指出常見的文體錯誤

  • govet 分析程式碼中的常見錯誤

  • staticcheck 各種靜態分析檢查

Lint Runners

我們推薦 golangci-lint 作為go-to lint的執行程式,這主要是因為它在較大的程式碼庫中的效能以及能夠同時配置和使用許多規範。這個repo有一個示例配置檔案.golangci.yml和推薦的linter設定。

golangci-lint 有various-linters可供使用。建議將上述linters作為基本set,我們鼓勵團隊新增對他們的專案有意義的任何附加linters。

Stargazers over time