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)中的錯誤與回滾
在交易中,我們會同時使用 error 與 panic/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 的旅程中,寫出乾淨、可靠的程式碼!