5.1 錯誤回傳與早期返回
5.1 錯誤回傳與早期返回
Go 把錯誤當成回傳值。這讓失敗路徑直接出現在程式碼裡,也讓呼叫者必須明確決定如何處理失敗。
本章目標
學完本章後,你將能夠:
- 理解
error回傳值的設計目的 - 用 early return 保持控制流程扁平
- 為錯誤加上足夠脈絡
- 在 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 configparse configvalidate 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}這段程式的層次很清楚:
- JSON 格式錯誤 → 400
- 欄位驗證錯誤 → 400
- 內部建立失敗 → 500
- 成功 → 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 | 給呼叫者穩定、可理解的訊息 |