本文 AI 產出,尚未審核

Golang – 控制流程

錯誤處理(error、panic、recover)


簡介

在任何程式語言中,錯誤處理都是保證程式穩定性與可維護性的關鍵。Go 語言採用了與其他語言截然不同的設計哲學:不使用例外(exception)機制作為日常錯誤的主要手段,而是以 error 介面 來傳遞可預期的錯誤,同時保留 panic / recover 供不可恢復的致命錯誤使用。了解這三者的差異與正確使用方式,能讓你寫出更安全、易除錯且符合 Go 社群慣例的程式。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,最後以實務案例帶你掌握 error、panic、recover 的完整運用,適合剛踏入 Go 世界的初學者,也能為已有基礎的開發者提供進一步的思考。


核心概念

1. error 介面 – 可預期錯誤的第一選擇

Go 把錯誤抽象為一個內建介面:

type error interface {
    Error() string
}

只要實作 Error() string 方法的型別,都可以當作 error 使用。標準函式庫大多返回 (T, error),呼叫端必須檢查 error 是否為 nil,才能繼續後續邏輯。

範例 1:檔案讀取的典型錯誤處理

package main

import (
    "fmt"
    "io/ioutil"
)

func readFile(path string) ([]byte, error) {
    data, err := ioutil.ReadFile(path) // 可能返回 error
    if err != nil {
        // 直接回傳錯誤給呼叫端
        return nil, fmt.Errorf("讀取檔案失敗: %w", err)
    }
    return data, nil
}

func main() {
    content, err := readFile("config.json")
    if err != nil {
        // 錯誤處理:印出訊息並退出
        fmt.Println(err)
        return
    }
    fmt.Printf("檔案內容: %s\n", content)
}

重點永遠檢查 error,不要忽略。如果真的不需要處理,可使用 _ = err 明確表達「我已知曉且不在意」。


2. panic – 致命錯誤的最後手段

panic 會立刻中斷目前的執行流程,向上層呼叫堆疊傳遞訊息,直到程式最外層(main)或被 recover 捕獲為止。panic 適合用在程式設計錯誤(例如不可能發生的狀況)或資源不可恢復的情況。

範例 2:不合法的參數導致 panic

package main

import "fmt"

func divide(a, b int) int {
    if b == 0 {
        panic("除數不能為零") // 致命錯誤
    }
    return a / b
}

func main() {
    fmt.Println(divide(10, 2)) // 正常
    fmt.Println(divide(10, 0)) // 觸發 panic,程式結束
}

提示:在公共函式庫中,除非真的無法繼續執行,否則盡量避免直接 panic,改以 error 回傳較為友好。


3. recover – 捕獲 panic 並恢復執行

recover 必須在 defer 函式中呼叫,才能捕捉到同一執行緒(goroutine)中拋出的 panic。若成功捕獲,recover 會回傳 panic 的值,否則回傳 nil。這讓我們可以在程式的最外層建立「保護層」來避免整個服務崩潰。

範例 3:使用 defer + recover 防止服務宕機

package main

import (
    "fmt"
    "log"
)

func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            // 捕獲 panic,轉成 error
            err = fmt.Errorf("捕獲 panic: %v", r)
            log.Println(err)
        }
    }()
    fn() // 可能會 panic
    return nil
}

func mayPanic() {
    panic("意外的錯誤")
}

func main() {
    if err := safeExecute(mayPanic); err != nil {
        fmt.Println("程式仍能繼續執行")
    }
    fmt.Println("後續業務邏輯")
}

關鍵recover 只能在 defer 中使用,且只能捕獲同一 goroutine 的 panic;跨 goroutine 必須自行傳遞錯誤訊息。


4. 三者的使用時機圖

狀況 建議使用 為什麼
輸入驗證、I/O 失敗、業務邏輯錯誤 error 可預期、呼叫端可自行決策
程式設計錯誤(如 nil pointer、陣列越界) panic 表示程式本身有 bug,應在測試階段修正
想保護服務不因單一 goroutine 崩潰 recover 搭配 defer 捕獲 panic,轉成錯誤或記錄,讓服務持續運作

常見陷阱與最佳實踐

1. 忽略 error

_ = someFunc() // ❌ 常見錯誤寫法

最佳實踐:明確檢查或記錄錯誤,若真的不需要,可寫成 if err := someFunc(); err != nil { /* log */ }

2. 在公共 API 中直接 panic

使用者會因為未捕獲 panic 而導致整個程式崩潰。建議:將錯誤包裝成 error,或在最外層提供 recover 包裝。

3. recover 只能捕獲同一 goroutine 的 panic

跨 goroutine 必須使用 channel 或 sync.WaitGroup 來傳遞錯誤訊息,不要期待 recover 能跨執行緒。

4. 把 panic 當成一般錯誤流程

panic 的成本較高(堆疊追蹤、runtime 清理),頻繁使用會降低效能。只在不可恢復的情況下使用

5. 忘記 defer 的執行順序

defer 會在函式返回前 逆序 執行,若有多個 defer,最內層的 recover 必須先於其他 defer 執行,否則無法捕獲 panic。


實際應用場景

A. Web 伺服器的全局錯誤攔截

net/http 中,我們常在最外層的 Handler 包裝一層 recover,確保單一請求的 panic 不會導致整個服務掛掉。

package main

import (
    "log"
    "net/http"
)

func recoverMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("捕獲 panic: %v", err)
                http.Error(w, "內部伺服器錯誤", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func riskyHandler(w http.ResponseWriter, r *http.Request) {
    panic("模擬的程式錯誤")
}

func main() {
    http.Handle("/", recoverMiddleware(http.HandlerFunc(riskyHandler)))
    log.Println("伺服器啟動於 :8080")
    http.ListenAndServe(":8080", nil)
}

B. 資料庫交易(Transaction)中的錯誤與回滾

在交易中,我們會同時使用 errorpanic/recover 來保證「要麼全部成功,要麼全部回滾」:

func withTransaction(db *sql.DB, fn func(tx *sql.Tx) error) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if p := recover(); p != nil {
            _ = tx.Rollback()
            err = fmt.Errorf("panic 觸發回滾: %v", p)
        } else if err != nil {
            _ = tx.Rollback()
        } else {
            err = tx.Commit()
        }
    }()
    return fn(tx) // 可能回傳 error,也可能 panic
}

C. CLI 工具的統一錯誤輸出

對於命令列工具,常在 main 中使用 defer recover,將 panic 轉成友善的錯誤訊息並返回非 0 結束碼。

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Fprintf(os.Stderr, "致命錯誤: %v\n", r)
            os.Exit(1)
        }
    }()
    // 主程式邏輯
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "執行失敗: %v\n", err)
        os.Exit(1)
    }
}

總結

  • error 是 Go 推薦的日常錯誤傳遞機制,應在所有可預期失敗的情境下使用。
  • panic 只留給程式設計錯誤或不可恢復的致命狀況,切勿濫用。
  • recover 必須配合 defer,用於捕獲 panic,讓服務在單一 goroutine 崩潰時仍能持續運作。
  • 正確的錯誤處理不僅提升程式的健全性,也符合 Go 社群的編碼風格與最佳實踐。

掌握以上三者的差異與適當的使用時機,你將能寫出 更安全、易除錯且具備彈性 的 Go 程式,無論是簡單的 CLI 工具、Web 服務,或是大型分散式系統,都能從容面對各種錯誤情境。祝你在 Golang 的旅程中,寫出乾淨、可靠的程式碼!