Uber Go 程式設計風格指南

FunTester發表於2025-02-08

簡介

本指南概述了在 Uber 編寫 Go 程式碼的約定和最佳實踐。目標是透過提供清晰的指南來管理程式碼複雜性,確保程式碼庫的可維護性,同時讓工程師能夠有效利用 Go 的特性。

所有程式碼都應透過 golintgo vet 檢查。建議在儲存時執行 goimports,並使用 golintgo vet 檢查錯誤。

指南

指向介面的指標

幾乎不需要使用指向介面的指標。即使底層資料是指標,介面也應作為值傳遞。

驗證介面合規性

在適當的地方編譯時驗證介面合規性,以確保型別實現了所需的介面。

type Handler struct {
  // ...
}

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

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

接收器和介面

帶有值接收器的方法可以在值和指標上呼叫,而帶有指標接收器的方法只能在指標或可定址的值上呼叫。

零值 Mutex 是有效的

sync.Mutexsync.RWMutex 的零值是有效的,因此很少需要指向 mutex 的指標。

var mu sync.Mutex
mu.Lock()

在邊界處複製切片和對映

切片和對映包含指向底層資料的指標,因此在複製時要小心,以避免意外的副作用。

使用 defer 清理資源

使用 defer 清理檔案、鎖等資源,確保即使發生錯誤,資源也能正確釋放。

p.Lock()
defer p.Unlock()

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

p.count++
return p.count

通道大小為一或無

通道的大小通常應為一或無緩衝。除非絕對必要,否則避免使用大緩衝區。

c := make(chan int, 1) // 或者
c := make(chan int)

列舉從 1 開始

列舉從 1 開始,以避免零值成為有效但非預期的狀態。

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

使用 "time" 處理時間

始終使用 time 包處理時間,以避免與時間計算相關的常見問題。

錯誤處理

錯誤型別

對於靜態錯誤訊息,使用 errors.New;對於動態錯誤訊息,使用 fmt.Errorf。對於需要匹配的錯誤,使用自定義錯誤型別。

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

func Open() error {
  return ErrCouldNotOpen
}

錯誤包裝

使用 fmt.Errorf%w 動詞包裝錯誤以提供上下文。

if err != nil {
  return fmt.Errorf("new store: %w", err)
}

錯誤命名

根據錯誤是否匯出,使用 Errerr 作為錯誤值的字首。

var (
  ErrBrokenLink = errors.New("link is broken")
  errNotFound   = errors.New("not found")
)

只處理一次錯誤

只處理一次錯誤。避免記錄錯誤後再返回它。

if err := emitMetrics(); err != nil {
  log.Printf("Could not emit metrics: %v", err)
}

處理型別斷言失敗

執行型別斷言時,始終使用 "comma ok" 慣用法以避免 panic。

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

不要 panic

在生產程式碼中避免使用 panic。相反,返回錯誤並讓呼叫者決定如何處理。

使用 go.uber.org/atomic

使用 go.uber.org/atomic 進行原子操作,以避免 sync/atomic 包中的常見錯誤。

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
    return
  }
  // 啟動 Foo
}

避免可變全域性變數

避免修改全域性變數。使用依賴注入代替。

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

避免在公共結構體中嵌入型別,以防止洩露實現細節。

避免使用內建名稱

避免使用 Go 的預宣告識別符號作為變數名,以防止遮蔽和混淆。

避免使用 init()

儘可能避免使用 init()。如果必須使用,請確保它是確定性的,並且不依賴於外部狀態。

main 中退出

僅在 main() 中呼叫 os.Exitlog.Fatal。所有其他函式應返回錯誤。

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

func run() error {
  // ...
}

在序列化結構體中使用欄位標籤

在序列化為 JSON、YAML 或其他格式的結構體中使用欄位標籤。

type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
}

不要啟動後不管的 Goroutine

確保 Goroutine 有明確的退出點,並正確清理。

var (
  stop = make(chan struct{})
  done = make(chan struct{})
)

go func() {
  defer close(done)
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

close(stop)
<-done

效能

優先使用 strconv 而不是 fmt

將基本型別轉換為字串時,使用 strconv 而不是 fmt,以獲得更好的效能。

避免重複的字串到位元組轉換

避免重複將相同的字串轉換為位元組切片。轉換一次並重用結果。

優先指定容器容量

儘可能指定切片和對映的容量,以避免不必要的分配。

data := make([]int, 0, size)

風格

避免過長的行

避免需要水平滾動的程式碼行。目標是軟限制為 99 個字元。

保持一致性

一致性是關鍵。在整個程式碼庫中遵循相同的風格。

分組相似的宣告

將相似的宣告分組以提高可讀性。

const (
  a = 1
  b = 2
)

var (
  a = 1
  b = 2
)

匯入分組順序

將匯入分為標準庫和第三方庫。

import (
  "fmt"
  "os"

  "go.uber.org/atomic"
)

包名

選擇簡短、描述性的包名,全部小寫且不為複數。

函式名

使用 MixedCaps 命名函式。測試函式可以包含下劃線以進行分組。

匯入別名

僅在必要時使用匯入別名以解決命名衝突。

函式分組和排序

按接收器分組函式,並按呼叫順序排序。

減少巢狀

透過提前處理錯誤情況和特殊情況來減少巢狀。

不必要的 else

當變數可以在單個 if 語句中設定時,避免不必要的 else 塊。

頂層變數宣告

除非型別不明顯,否則使用 var 進行頂層變數宣告。

未匯出的全域性變數字首為 _

為避免意外使用,未匯出的頂層變數和常量應字首為 _

結構體中的嵌入

僅在提供實際好處時才在結構體中嵌入型別。避免嵌入互斥鎖。

區域性變數宣告

儘可能使用短變數宣告 (:=) 宣告區域性變數。

nil 是有效的切片

使用 nil 表示空切片,而不是顯式返回空切片。

減少變數作用域

儘可能減少變數的作用域以提高可讀性。

避免裸引數

避免在函式呼叫中使用裸引數。使用註釋或命名型別以提高畫質晰度。

使用原始字串字面量避免轉義

使用原始字串字面量以避免字串中的跳脫字元。

初始化結構體

使用欄位名初始化結構體

初始化結構體時始終使用欄位名。

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

省略結構體中的零值欄位

初始化結構體時省略零值欄位。

user := User{
  FirstName: "John",
  LastName:  "Doe",
}

使用 var 宣告零值結構體

使用 var 宣告零值結構體。

var user User

初始化結構體引用

初始化結構體引用時使用 &T{} 而不是 new(T)

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

初始化對映

使用 make 初始化空對映,使用對映字面量初始化具有固定元素的對映。

m := make(map[T1]T2, size)

Printf 外部宣告格式字串

Printf 風格的函式外部宣告格式字串為 const 值。

命名 Printf 風格的函式

命名 Printf 風格的函式時使用 f 字尾以啟用 go vet 檢查。

模式

測試表

使用帶有子測試的表驅動測試來避免重複程式碼。

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

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

函式式選項

在建構函式和公共 API 中使用函式式選項來處理可選引數。

type Option interface {
  apply(*options)
}

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

func Open(addr string, opts ...Option) (*Connection, error) {
  // ...
}

程式碼檢查

在整個程式碼庫中使用一致的程式碼檢查工具。推薦的程式碼檢查工具包括:

  • errcheck
  • goimports
  • golint
  • govet
  • staticcheck

程式碼檢查執行器

使用 golangci-lint 作為 Go 程式碼的程式碼檢查執行器。它支援許多程式碼檢查工具,並可以透過 .golangci.yml 檔案進行配置。

linters:
  enable:
    - errcheck
    - goimports
    - golint
    - govet
    - staticcheck

本指南提供了在 Uber 編寫 Go 程式碼的全面最佳實踐。透過遵循這些指南,您可以確保程式碼的可維護性、高效性和符合 Go 的習慣用法。

FunTester 原創精華

【連載】從 Java 開始效能測試

  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。

相關文章