Go:慣用高效率參考指南
目錄
1. 錯誤處理 {#errors}
// ❌ 忽略錯誤
result, _ := os.Open(path)
// ✅ — 始終處理;只有在錯誤確實無關時才使用 _
result, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
// ❌ 冗餘的錯誤變數
err := doA()
if err != nil { return err }
err = doB()
if err != nil { return err }
// ✅ — 每個 :=/: 都可以;這是慣用的 Go。不要試圖「修正」它。
// 你可以簡化的:將單一 return 的 if 主體合併成一行
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// ❌ 無附加價值的自訂錯誤類型
type MyError struct{ msg string }
func (e MyError) Error() string { return e.msg }
// ✅ — 除非呼叫者需要檢查類型,否則使用 errors.New 或 fmt.Errorf
var ErrNotFound = errors.New("not found")
return fmt.Errorf("lookup %q: %w", key, ErrNotFound)
用 %w(而不是 %v)包裝錯誤,以便呼叫者可以使用 errors.Is / errors.As。
2. 切片與映射 {#slices}
// ❌ 在已知大小的情況下,未預先分配即增長切片
var result []string
for _, item := range items {
result = append(result, item.Name)
}
// ✅
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.Name)
}
// ❌ 在寫入映射前手動檢查是否存在
if _, ok := m[key]; !ok {
m[key] = []string{}
}
m[key] = append(m[key], value)
// ✅ — 對 nil 切片進行 append 是合法的 Go
m[key] = append(m[key], value)
// ❌ 透過賦值複製映射(複製的是參考)
copy := original
// ✅
copy := make(map[K]V, len(original))
for k, v := range original { copy[k] = v }
3. Goroutine 與 Channel {#concurrency}
// ❌ 不追蹤生命週期的 fire-and-forget goroutine
go doWork()
// ✅ — 使用 errgroup 或 WaitGroup 來追蹤完成狀態
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
wg.Wait()
// ❌ 無緩衝 channel 造成不必要的 goroutine 阻塞
ch := make(chan Result)
go func() { ch <- compute() }()
result := <-ch
// ✅ — 對於單一結果,使用緩衝 channel 可以避免接收者提早退出時 goroutine 洩漏
ch := make(chan Result, 1)
go func() { ch <- compute() }()
result := <-ch
// ❌ 帶有 busy-wait default 的 select
for {
select {
case v := <-ch:
process(v)
default:
// 空轉
}
}
// ✅ — 除非真的需要非阻塞,否則使用阻塞 select
for v := range ch {
process(v)
}
使用 golang.org/x/sync/errgroup 進行 fan-out 並收集錯誤。
4. 結構體與介面 {#structs}
// ❌ 龐大的介面
type Storage interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
Delete(key string) error
List(prefix string) ([]string, error)
// ... 還有 10 個方法
}
// ✅ — 小型、可組合的介面
type Getter interface { Get(key string) ([]byte, error) }
type Setter interface { Set(key string, val []byte) error }
type Storage interface { Getter; Setter }
// ❌ 從建構函式返回具體結構(將呼叫者綁定到實作)
func NewStore() *RedisStore { ... }
// ✅ — 當你擁有或預期有多種實作時,返回介面
func NewStore() Storage { return &RedisStore{...} }
// ❌ 對小型值類型使用指標接收器
func (p *Point) X() float64 { return p.x }
// ✅ — 對小型不可變類型使用值接收器
func (p Point) X() float64 { return p.x }
規則:當方法會修改狀態或結構體很大(超過 3 個非平凡大小的欄位)時使用指標接收器。否則使用值接收器。
5. 函式與閉包 {#functions}
// ❌ 僅為了避免宣告變數而使用命名回傳值
func divide(a, b float64) (result float64, err error) {
result = a / b
return
}
// ✅ — 命名回傳值僅在需要延遲修改或作為文件說明時才有價值
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
// ❌ 閉包捕獲迴圈變數(經典的 Go 錯誤,在 Go 1.22+ 中已修正)
// Go <1.22:每個 goroutine 捕獲同一個 i
for i := 0; i < n; i++ {
go func() { use(i) }()
}
// ✅ (Go <1.22 — 作為參數傳遞)
for i := 0; i < n; i++ {
go func(i int) { use(i) }(i)
}
// Go 1.22+:迴圈變數的作用域在每次迭代中獨立,因此原始寫法是安全的
6. Go 特有的反模式 {#antipatterns}
| 反模式 | 建議寫法 |
|---|---|
if err != nil { return err } 重複 5 次以上 | 可接受——這是慣用的 Go |
對預期內的錯誤使用 panic | return err |
帶有副作用的 init() | 在 main 或建構函式中進行明確初始化 |
不使用泛型的 interface{} / any | 使用泛型 (Go 1.18+) 或具型別的介面 |
| Mutex 欄位未與其保護的資料相鄰 | 將 mu 直接放在它所保護的欄位上方 |
| Channel of channels | 通常是過度設計的跡象;重新設計 |
在測試中使用 time.Sleep | 使用 testing 鉤子或 channel 進行同步 |
| 匯出類型包含未匯出欄位(當欄位是核心時) | 使用所有欄位都匯出的 record 風格結構體 |
在 main 之外使用 log.Fatal | 將錯誤沿著呼叫堆疊向上回傳 |
限制
- 這些是語言特定的指南,不涵蓋整體架構決策。
- 過度壓縮可能會降低可讀性;請自行判斷應用。