Go 把錯誤當成回傳值。這讓失敗路徑直接出現在程式碼裡,也讓呼叫者必須明確決定如何處理失敗。

本章目標

學完本章後,你將能夠:

  1. 理解 error 回傳值的設計目的
  2. 用 early return 保持控制流程扁平
  3. 為錯誤加上足夠脈絡
  4. 在 HTTP handler 中對應不同錯誤情境

【觀察】Go 錯誤處理很顯式

Go 錯誤處理的核心規則是:可能失敗的函式用 error 回傳失敗,呼叫者在呼叫點立即處理。常見寫法如下:

1data, err := os.ReadFile("config.json")
2if err != nil {
3    return err
4}

這段程式很直接:讀檔可能失敗,失敗就回傳錯誤。

對剛接觸 Go 的人來說,if err != nil 可能看起來重複。但這個重複有明確目的:失敗路徑不被隱藏,讀者可以逐步看見每個操作失敗時會發生什麼事。

【判讀】錯誤是控制流程的一部分

Go 的錯誤模型把失敗視為控制流程的一部分。很多語言用 exception 讓錯誤跳出目前流程;Go 則偏好讓錯誤留在函式簽名和呼叫點:

 1func LoadConfig(path string) (Config, error) {
 2    data, err := os.ReadFile(path)
 3    if err != nil {
 4        return Config{}, fmt.Errorf("read config: %w", err)
 5    }
 6
 7    var cfg Config
 8    if err := json.Unmarshal(data, &cfg); err != nil {
 9        return Config{}, fmt.Errorf("parse config: %w", err)
10    }
11
12    if err := validateConfig(cfg); err != nil {
13        return Config{}, fmt.Errorf("validate config: %w", err)
14    }
15
16    return cfg, nil
17}

錯誤脈絡的核心規則是:越靠近失敗來源,越應補上「正在做什麼」的資訊。這裡每個錯誤都被加上脈絡:

  • read config
  • parse config
  • validate config

當錯誤出現在 log 裡時,讀者不只知道失敗了,也知道失敗在哪個階段。

【策略】用 early return 避免巢狀

early return 的核心規則是:失敗路徑就地返回,成功路徑保持在左側。不要把成功路徑包在很多層 else 裡:

 1// 不佳:成功路徑被包在巢狀中
 2func Load(path string) (Config, error) {
 3    data, err := os.ReadFile(path)
 4    if err == nil {
 5        var cfg Config
 6        err = json.Unmarshal(data, &cfg)
 7        if err == nil {
 8            return cfg, nil
 9        } else {
10            return Config{}, err
11        }
12    } else {
13        return Config{}, err
14    }
15}

Go 更常用 early return:

 1func Load(path string) (Config, error) {
 2    data, err := os.ReadFile(path)
 3    if err != nil {
 4        return Config{}, err
 5    }
 6
 7    var cfg Config
 8    if err := json.Unmarshal(data, &cfg); err != nil {
 9        return Config{}, err
10    }
11
12    return cfg, nil
13}

成功路徑保持在左側,失敗路徑就地處理。這是 Go 可讀性的重要風格。

【執行】HTTP handler 中的錯誤路徑

邊界層錯誤處理的核心規則是:內部錯誤要轉成呼叫者能理解的回應。HTTP handler 要把 Go 的錯誤轉成 HTTP response:

 1func handleCreateUser(w http.ResponseWriter, r *http.Request) {
 2    var req CreateUserRequest
 3    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
 4        writeJSONError(w, http.StatusBadRequest, "invalid JSON")
 5        return
 6    }
 7
 8    if req.Name == "" {
 9        writeJSONError(w, http.StatusBadRequest, "name is required")
10        return
11    }
12
13    user, err := createUser(req)
14    if err != nil {
15        writeJSONError(w, http.StatusInternalServerError, "create user failed")
16        return
17    }
18
19    writeJSON(w, http.StatusCreated, user)
20}

這段程式的層次很清楚:

  1. JSON 格式錯誤 → 400
  2. 欄位驗證錯誤 → 400
  3. 內部建立失敗 → 500
  4. 成功 → 201

每個錯誤路徑都結束於 return,後面的成功流程不需要被 else 包住。

錯誤訊息要包含脈絡

錯誤訊息分層的核心規則是:內部 error 保留診斷脈絡,對外 response 保持穩定且不洩漏內部細節。底層函式應該保留技術脈絡:

1return fmt.Errorf("insert user %q: %w", req.Name, err)

但對外 response 不一定要暴露內部錯誤:

1writeJSONError(w, http.StatusInternalServerError, "create user failed")

這是兩層不同需求:

層次目標
內部 error幫工程師定位問題
對外 response給呼叫者穩定、可理解的訊息