常見錯誤

Go 語言常見錯誤與最佳實踐。

1. Unintended variable shadowing

變數遮蔽

當變數在內部區塊被重新宣告時,可能會導致錯誤或混淆,因為可能不小心引用到錯誤的變數。應避免使用同樣名稱的變數,以防止混淆。

2. Unnecessary nested code

不必要的巢狀程式碼

過度巢狀的程式碼會增加理解難度。應該保持「happy path」(正向流程)靠左並提早返回,減少巢狀層次。避免在 if-else 中使用 else,並盡量在不符合條件時直接返回。

3. Misusing init functions

init 函數用於初始化,但其錯誤處理有限,並可能使狀態處理和測試變得複雜。初始化應該儘量使用專門的函數來處理,避免全域變數和不必要的依賴。

4. Overusing getters and setters

在 Go 中,強制使用 Getters 和 Setters 並不是慣用做法。如果它們不增加價值,就不應該使用。Go 提倡簡潔的設計,應該平衡效率與遵循編程習慣。

5. Interface pollution

介面污染

不應該提前創建不必要的抽象介面,應在真正需要時才創建介面,避免增加複雜性。好的抽象應當是被發現而非被強行設計的。

6. Interface on the producer side

在 Go 中,介面實現是隱式的,通常應由消費者決定是否需要介面,避免由生產者強加抽象。如果確實需要生產者提供介面,應保持其最小化,以提高重用性與組合性。

7. Returning interfaces

accept interfaces, return structs

  1. 函式應該回傳具體類型,而不是介面
  2. 函式應該接收介面作為參數

8. any says nothing

  • any 使用時機僅限於確實需要接受或返回任意型別的場景(如 json.Marshal
  • 不當使用 any 的缺點:
    • 無法提供有意義的型別資訊
    • 可能導致編譯時期的問題
    • 允許呼叫者使用任意型別資料
  • 避免過度泛化程式碼
  • 適度的程式碼重複可能比使用 any 更好
  • 使用 any 前應先評估是否真的需要處理任意型別

9. Being confused about when to use generics

不建議使用泛型的情況

  1. 只是呼叫型別參數的方法時
// Not Good: 不需要泛型,直接使用 interface 即可
func foo[T io.Writer](w T) {
    b := getBytes()
    _, _ = w.Write(b)
}

// Good: 直接使用 interface
func foo(w io.Writer) {
    b := getBytes()
    _, _ = w.Write(b)
}
  1. 使程式碼更複雜時
// Not Good: 過度抽象化
func doSomething[T any](item T) T {
    return item
}

建議使用泛型的情況

  1. 通用資料結構
// Good: 泛型資料結構
type Node[T any] struct {
    Val  T
    next *Node[T]
}

func (n *Node[T]) Add(next *Node[T]) {
    n.next = next
}
  1. 處理 slice, map, channel 等通用型別
// Good: 合併任意型別的 channel
func merge[T any](ch1, ch2 <-chan T) <-chan T {
    // implementation
}

// Good: 取得任意型別 map 的 keys
func getKeys[K comparable, V any](m map[K]V) []K {
    var keys []K
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}
  1. 抽象行為模式
// Good: 通用排序行為
type sliceFn[T any] struct {
    s       []T
    compare func(T, T) bool
}

func (s sliceFn[T]) Len() int           { return len(s.s) }
func (s sliceFn[T]) Less(i, j int) bool { return s.compare(s.s[i], s.s[j]) }
func (s sliceFn[T]) Swap(i, j int)      { s.s[i], s.s[j] = s.s[j], s.s[i] }

10. Not being aware of the possible problems with type embedding

Issue 1: 簡單 Embedding

  • 僅為了語法糖而使用(foo.Baz vs foo.Bar.Baz
  • 違反了「不應僅為簡化訪問而使用 embedding」的原則
type Foo struct {
    Bar
}
type Bar struct {
    Baz int
}

Issue 2: Mutex Embedding

  • 暴露了同步機制(Lock/Unlock 方法)
  • 客戶端可以直接操作鎖,破壞封裝性
  • 應該使用組合而非嵌入
// 不推薦
type InMem struct {
    sync.Mutex
    m map[string]int
}

// 推薦
type InMem struct {
    mu   sync.Mutex
    data map[string]string
}

Issue 3: 建構函數與接收器

  • 更具描述性的命名(writer vs writeCloser)
  • 添加建構函數
  • 使用指標接收器
type Logger struct {
    writer io.WriteCloser
}

// 建議:Pointer Receiver
func (l *Logger) Write(p []byte) (int, error) {
    return l.writer.Write(p)
}

func (l *Logger) Close() error {
    return l.writer.Close()
}