簡介
本指南概述了在 Uber 編寫 Go 程式碼的約定和最佳實踐。目標是透過提供清晰的指南來管理程式碼複雜性,確保程式碼庫的可維護性,同時讓工程師能夠有效利用 Go 的特性。
所有程式碼都應透過 golint
和 go vet
檢查。建議在儲存時執行 goimports
,並使用 golint
和 go vet
檢查錯誤。
指南
指向介面的指標
幾乎不需要使用指向介面的指標。即使底層資料是指標,介面也應作為值傳遞。
驗證介面合規性
在適當的地方編譯時驗證介面合規性,以確保型別實現了所需的介面。
type Handler struct {
// ...
}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// ...
}
接收器和介面
帶有值接收器的方法可以在值和指標上呼叫,而帶有指標接收器的方法只能在指標或可定址的值上呼叫。
零值 Mutex 是有效的
sync.Mutex
和 sync.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)
}
錯誤命名
根據錯誤是否匯出,使用 Err
或 err
作為錯誤值的字首。
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.Exit
或 log.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 自動化
- 理論、感悟、影片