目錄
- 什麼是 Go 的錯誤處理?
- error 介面與基本用法
- 錯誤包裝(Error Wrapping)
- errors.Is 與 errors.As
- 自訂 error 型別
- Sentinel Errors 設計模式
- panic / recover 與 defer
- defer 執行順序與陷阱
- 實戰範例
- 最佳實踐
- 常見問題
- 總結
- 參考資源
什麼是 Go 的錯誤處理?
Go 採用錯誤即值(errors are values)的設計哲學 — 錯誤是普通的 return value,不是 exception。
核心特點
- 🎯 顯式處理:每個可能出錯的函式都回傳
error,呼叫端必須處理 - ⚡ 無 try/catch:沒有 exception,控制流清晰
- 🔧 可組合:error 是介面,可以 wrap、檢查、轉型
- 📦 panic 保留給「程式設計錯誤」:array 越界、nil 指標 → panic;I/O 失敗 → return error
為什麼這樣設計?
傳統 exception 的問題:
- 控制流隱藏(任何函式可能丟 exception)
- 錯誤類型編譯時不可見
- 忘了 catch 會炸到最外層
Go 的做法:
result, err := doSomething()
if err != nil {
return fmt.Errorf("doSomething failed: %w", err)
}
// 用 result
每行都明確處理錯誤,沒有「驚喜」。
error 介面與基本用法
error 介面定義
整個 Go 錯誤系統只建立在這一個介面上:
type error interface {
Error() string
}
任何有 Error() string 方法的型別都是 error。
產生 error 的三種方式
import (
"errors"
"fmt"
)
// 1. errors.New — 最簡單的純文字錯誤
err1 := errors.New("檔案不存在")
// 2. fmt.Errorf — 含格式化參數
filename := "config.json"
err2 := fmt.Errorf("檔案 %s 不存在", filename)
// 3. fmt.Errorf 含 %w 動詞 → 包裝 error
err3 := fmt.Errorf("讀取 config 失敗: %w", err1)
標準回傳模式
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除以零")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("錯誤:", err)
return
}
fmt.Println(result)
}
慣例:error 是最後一個 return value
// ✅ Go 慣例:error 放最後
func ReadConfig(path string) (Config, error)
func ParseInt(s string) (int, error)
// ❌ 違反慣例
func ReadConfig(path string) (error, Config)
錯誤包裝(Error Wrapping)
從 Go 1.13 起,fmt.Errorf 的 %w 動詞會包裝底層 error,保留原始錯誤鏈。
%w vs %v 的差別
baseErr := errors.New("connection refused")
// %v 只取出文字(會斷鏈)
wrapped1 := fmt.Errorf("無法連線資料庫: %v", baseErr)
// wrapped1 內部沒有保留 baseErr 的參考
// %w 包裝(保留鏈)
wrapped2 := fmt.Errorf("無法連線資料庫: %w", baseErr)
// wrapped2 可以用 errors.Unwrap / errors.Is / errors.As 找回 baseErr
兩者輸出的字串都是 "無法連線資料庫: connection refused",但鏈結結構不同。
多層包裝
func loadConfig() error {
err := readFile("config.json")
if err != nil {
return fmt.Errorf("loadConfig: %w", err)
}
return nil
}
func startServer() error {
err := loadConfig()
if err != nil {
return fmt.Errorf("startServer: %w", err)
}
return nil
}
// main 拿到的 error 文字會是:
// startServer: loadConfig: open config.json: no such file or directory
每一層都加上自己的 context,最終 error 訊息含完整呼叫鏈。
Unwrap
err := fmt.Errorf("外層: %w", errors.New("內層"))
inner := errors.Unwrap(err)
// inner.Error() == "內層"
通常不直接用 Unwrap,而用 errors.Is / errors.As(見下節)。
errors.Is 與 errors.As
%w 把 error 鏈起來後,怎麼判斷鏈裡是否有特定 error?用 errors.Is 與 errors.As。
errors.Is — 判斷是否為特定錯誤
import (
"errors"
"io"
)
func readFile(path string) error {
// 假設這裡讀檔回傳 io.EOF
return io.EOF
}
func main() {
err := readFile("/tmp/foo")
// ❌ 老派寫法(會錯)
if err == io.EOF {
// 如果 err 被 wrap 過,這個判斷失敗
}
// ✅ 正確寫法
if errors.Is(err, io.EOF) {
fmt.Println("檔案結束")
}
}
errors.Is 會沿著 wrap 鏈檢查,任何一層 match 就回 true。
errors.As — 取出特定型別
type PathError struct {
Path string
Op string
Err error
}
func (e *PathError) Error() string {
return fmt.Sprintf("%s %s: %v", e.Op, e.Path, e.Err)
}
func main() {
err := someFunc()
var pathErr *PathError
if errors.As(err, &pathErr) {
// 鏈裡有 *PathError,pathErr 已指向它
fmt.Println("路徑:", pathErr.Path)
}
}
Is vs As 比較
| 需求 | 用 |
|---|---|
「這個錯誤是不是 io.EOF / sql.ErrNoRows?」 |
errors.Is |
「我要取出 wrap 內的 *MyError 結構讀欄位」 |
errors.As |
| 兩者都可以 | 偏好 errors.Is(更簡潔) |
自訂 error 型別
當錯誤需要攜帶額外資料時(如 status code、欄位名),定義自訂 error 型別。
基本範例:欄位驗證錯誤
type ValidationError struct {
Field string
Value any
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("欄位 %q (值 %v): %s", e.Field, e.Value, e.Message)
}
// 使用
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Value: age,
Message: "必須在 0-150 之間",
}
}
return nil
}
func main() {
err := validateAge(-5)
var vErr *ValidationError
if errors.As(err, &vErr) {
fmt.Println("欄位:", vErr.Field) // age
fmt.Println("錯誤值:", vErr.Value) // -5
}
}
含底層錯誤的自訂型別
實作 Unwrap() 方法讓 errors.Is/As 能穿透:
type DBError struct {
Query string
Err error // 包裝底層錯誤
}
func (e *DBError) Error() string {
return fmt.Sprintf("db query failed: %s: %v", e.Query, e.Err)
}
// 實作 Unwrap,讓 errors.Is 可以穿透
func (e *DBError) Unwrap() error {
return e.Err
}
// 使用
err := &DBError{
Query: "SELECT * FROM users",
Err: sql.ErrNoRows,
}
errors.Is(err, sql.ErrNoRows) // true(穿透到 Unwrap)
用 pointer receiver 還是 value receiver?
慣例:自訂 error 用 pointer receiver。
// ✅ Pointer receiver — 推薦
func (e *MyError) Error() string { ... }
return &MyError{...}
// ⚠️ Value receiver — 可以但少見
func (e MyError) Error() string { ... }
return MyError{...}
理由:
- Pointer 比較不會誤觸發 nil interface 陷阱(見常見問題)
- 大多數標準庫的 error 型別都用 pointer
Sentinel Errors 設計模式
Sentinel error:在套件層級宣告的固定錯誤值,給呼叫端對照用。
範例:標準庫的 sentinel
// io 套件
var EOF = errors.New("EOF")
// sql 套件
var ErrNoRows = errors.New("sql: no rows in result set")
呼叫端可以對照:
rows, err := db.Query(...)
if errors.Is(err, sql.ErrNoRows) {
// 沒結果,不算錯
return nil, nil
}
何時用 sentinel vs 自訂型別?
| 情境 | 用 |
|---|---|
| 錯誤是「狀態」,無額外資料 | Sentinel(io.EOF) |
| 錯誤有額外資料要傳 | 自訂型別(PathError) |
| 介面回傳值 | Sentinel(呼叫端好對照) |
別把 sentinel 寫成可變
// ❌ 用 var 但可能被誤改
var ErrNotFound = errors.New("not found")
// 更嚴謹:建立後不要曝露在 mutable 環境
// (Go 沒有 const error,所以 var 是慣例做法)
panic / recover 與 defer
panic 不是 exception
panic 在 Go 是「程式設計錯誤」的訊號,不是業務錯誤。
| 場景 | 用 |
|---|---|
| 檔案開啟失敗 / 網路錯誤 / 使用者輸入錯 | return error |
| nil 指標 dereference / 陣列越界 / 不變式被違反 | Runtime 自動 panic |
| 程式設計者明確標示「不該發生」 | 手動 panic("invariant violated") |
panic 的傳播行為
func a() {
panic("出事了")
}
func b() {
a() // panic 從 a 往上傳
}
func main() {
b() // 不處理,程式直接 crash + 印 stack trace
}
panic 會沿 call stack 往上傳,沿途的 defer 仍會執行,直到遇到 recover。
recover:唯一能攔截 panic 的方法
recover 只能在 defer 內呼叫才有效。
func safeRun() {
defer func() {
if r := recover(); r != nil {
fmt.Println("攔截到 panic:", r)
}
}()
panic("出事了")
}
func main() {
safeRun()
fmt.Println("safeRun 結束,但 main 繼續跑")
}
何時該用 recover?
很少。合理用途:
- 頂層 server 攔截每個 request 的 panic(避免整個 process 死)
- goroutine 隔離:一個 goroutine panic 不該拖死整個 process
- 第三方 callback 不可信時包裹起來
// 範例:HTTP middleware
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("panic in handler: %v\n%s", rec, debug.Stack())
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
⚠️ 不要用 panic/recover 模擬 exception
// ❌ 反模式 — 不要這樣用 Go
func parseFile(path string) (result Result) {
defer func() {
if r := recover(); r != nil {
result = Result{Error: r.(error)}
}
}()
if !exists(path) {
panic(errors.New("not found")) // 應該 return error
}
// ...
}
defer 執行順序與陷阱
LIFO 順序
defer 是 Last-In-First-Out(後進先出):
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("main")
}
// 輸出:
// main
// 3
// 2
// 1
參數在 defer 宣告時求值
func main() {
x := 10
defer fmt.Println("x =", x) // 求值:x = 10
x = 20
// 函式結束時印:x = 10(不是 20)
}
但如果用 closure 捕獲:
func main() {
x := 10
defer func() {
fmt.Println("x =", x) // closure 在執行時求值
}()
x = 20
// 印:x = 20
}
defer + 修改 named return value
func addOne() (n int) {
defer func() {
n++ // 修改 named return
}()
return 5
// 實際 return 6
}
巧用在「retry / cleanup 後改 return」。
defer 的常見模式:清理資源
func process(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 函式結束自動關
// 處理 f ...
return nil
}
defer 在迴圈中的陷阱
// ❌ 在迴圈內 defer Close,所有檔案開啟期間都不會關
for _, path := range paths {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 累積!函式結束才一起 close
// ...
}
// ✅ 包成 function
for _, path := range paths {
if err := processOne(path); err != nil {
return err
}
}
func processOne(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // 本 function 結束就 close
// ...
return nil
}
實戰範例
範例 1:完整的錯誤處理鏈
package main
import (
"errors"
"fmt"
"os"
)
// 自訂 error 型別
type ConfigError struct {
Path string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("config %s: %v", e.Path, e.Err)
}
func (e *ConfigError) Unwrap() error {
return e.Err
}
// 套件層級 sentinel
var ErrConfigMissing = errors.New("config 檔案缺少必要欄位")
// 包裝的低階函式
func readConfigFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, &ConfigError{Path: path, Err: err}
}
return data, nil
}
// 高階函式
func loadConfig(path string) error {
_, err := readConfigFile(path)
if err != nil {
return fmt.Errorf("loadConfig: %w", err)
}
return nil
}
func main() {
err := loadConfig("/no/such/path.json")
if err == nil {
return
}
fmt.Println("錯誤:", err)
// 輸出:loadConfig: config /no/such/path.json: open /no/such/path.json: no such file or directory
// 用 errors.Is 檢查底層
if errors.Is(err, os.ErrNotExist) {
fmt.Println("確認是檔案不存在")
}
// 用 errors.As 取出自訂型別
var cfgErr *ConfigError
if errors.As(err, &cfgErr) {
fmt.Println("Config 路徑:", cfgErr.Path)
}
}
範例 2:HTTP handler 加 recover
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
)
func recoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
log.Printf("[panic] %v\n%s", rec, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func handleRisky(w http.ResponseWriter, r *http.Request) {
var p *int
fmt.Fprintln(w, *p) // nil pointer → panic
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/risky", handleRisky)
log.Fatal(http.ListenAndServe(":8080", recoverMiddleware(mux)))
}
範例 3:goroutine 內 panic 隔離
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("[goroutine panic] %v\n%s", r, debug.Stack())
}
}()
fn()
}()
}
func main() {
safeGo(func() {
panic("worker panicked")
})
time.Sleep(time.Second)
fmt.Println("main 還活著")
}
最佳實踐
1. 永遠檢查 error,別用 _ 偷懶
// ❌ 拋棄錯誤
data, _ := os.ReadFile(path)
// ✅ 處理
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
2. 包裝時加 context
// ❌ 訊息沒幫助
if err != nil {
return err
}
// ✅ 告訴呼叫端「在做什麼時失敗」
if err != nil {
return fmt.Errorf("loadUser id=%s: %w", id, err)
}
3. 不要在多處包同樣訊息
// ❌ 訊息累積冗餘
return fmt.Errorf("loadUser: %w", err)
// 在多層都這樣寫 → "loadUser: loadUser: loadUser: not found"
// ✅ 每層加自己的 context(不重複)
return fmt.Errorf("loadUser id=%s: %w", id, err)
4. 對外 API 用 sentinel 或定型錯誤,內部用 wrap
// 套件對外
var (
ErrNotFound = errors.New("not found")
ErrInvalid = errors.New("invalid input")
)
// 內部實作
func getUser(id string) (*User, error) {
u, err := db.Query(id)
if err == sql.ErrNoRows {
return nil, ErrNotFound // 轉成套件對外的 sentinel
}
if err != nil {
return nil, fmt.Errorf("db query: %w", err)
}
return u, nil
}
5. panic 只用於不變式違反
// ✅ 合理:邏輯不該到這
func direction(d int) string {
switch d {
case 1: return "north"
case 2: return "south"
case 3: return "east"
case 4: return "west"
}
panic(fmt.Sprintf("invalid direction: %d", d))
}
// ❌ 不該:使用者輸入錯誤該 return error
func parseAge(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(err) // 不對!應該 return (int, error)
}
return n
}
6. defer 寫在資源取得之後
// ✅ 立刻 defer
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
// ❌ 中間插入別的邏輯
f, err := os.Open(path)
if err != nil {
return err
}
// 一堆別的邏輯 ...
defer f.Close() // 不該晚這麼多
常見問題
問題 1:nil error 比較失敗
症狀:
var err error = (*MyError)(nil)
if err == nil {
fmt.Println("err 是 nil")
} else {
fmt.Println("err 不是 nil") // 印出這個!
}
原因:err 介面值非 nil(雖然底層 pointer 是 nil,但介面有「型別」資訊)
解決:
// ❌ 不要回傳具名 nil pointer 當 error
func foo() error {
var e *MyError = nil
return e // 介面值非 nil
}
// ✅ 明確回傳 nil
func foo() error {
return nil
}
問題 2:errors.Is 對自訂型別不生效
症狀:
type MyErr struct{ Code int }
func (e *MyErr) Error() string { return "..." }
err1 := &MyErr{Code: 1}
err2 := &MyErr{Code: 1}
errors.Is(err1, err2) // false!
原因:errors.Is 預設用 == 比較。&MyErr{1} 兩次建立的 pointer 不同。
解決:實作 Is 方法
func (e *MyErr) Is(target error) bool {
t, ok := target.(*MyErr)
if !ok {
return false
}
return e.Code == t.Code
}
問題 3:defer 沒執行
症狀:函式結束但 defer 沒跑
可能原因:
- 用
os.Exit()直接結束(不會跑 defer) - fatal panic(如 unrecovered 從別處傳上來且未被 recover)
// ❌ os.Exit 跳過 defer
func main() {
defer fmt.Println("cleanup")
os.Exit(1) // defer 不會跑
}
// ✅ 用 return / panic
func main() {
defer fmt.Println("cleanup")
return
}
問題 4:recover 沒攔截到 panic
檢查清單:
recover()必須在defer內呼叫- defer 必須在 panic 發生的同一個 goroutine
- defer 必須在 panic 之前已宣告
// ❌ goroutine 內的 panic 主 goroutine 攔不到
func main() {
defer func() {
recover() // 攔不到 goroutine 內的 panic
}()
go func() {
panic("子 goroutine 出事")
}()
time.Sleep(time.Second)
}
// ✅ 在子 goroutine 內 recover
go func() {
defer func() {
if r := recover(); r != nil {
log.Println(r)
}
}()
panic("...")
}()
問題 5:%w 包了之後 error 訊息變很長
原因:每層 wrap 都加自己的訊息,多層後變很冗
緩解:
- 每層只加新資訊,不重複上一層已有的
- 對外 API 不暴露 wrap 細節(轉成 sentinel 或自訂型別)
問題 6:return err vs return fmt.Errorf("...: %w", err)
判斷:
- 該層有新增 context(如「在 loadUser 時」)→ wrap
- 純轉發、無新增資訊 → 直接
return err
// 直接轉發 OK
func wrapper() error {
return inner()
}
// 加 context 才 wrap
func loadUser(id string) error {
if err := inner(); err != nil {
return fmt.Errorf("loadUser id=%s: %w", id, err)
}
return nil
}
總結
核心要點
Go 錯誤處理 = error 介面 + errors.Is/As + %w 包裝 + 自訂型別 + sentinel
panic 只給「不該發生」用,recover 只在頂層攔截
defer 是 LIFO,參數宣告時求值
設計原則:
- ✅ 錯誤是值,return 而非 throw
- ✅ 每層 wrap 加 context(用
%w) - ✅ 對外 API 用 sentinel / 定型 error
- ✅ panic 只給程式設計錯誤
- ✅ recover 用在頂層攔截(HTTP middleware、goroutine 隔離)
速查表
// 建立 error
errors.New("msg")
fmt.Errorf("msg %d", n)
fmt.Errorf("ctx: %w", err) // 包裝
// 檢查
errors.Is(err, io.EOF)
errors.As(err, &myErr)
errors.Unwrap(err)
// 自訂型別
type MyErr struct { ... }
func (e *MyErr) Error() string { ... }
func (e *MyErr) Unwrap() error { return e.inner }
func (e *MyErr) Is(t error) bool { ... } // 選擇性
// panic / recover
defer func() {
if r := recover(); r != nil {
// 處理
}
}()
何時用什麼速查
| 情境 | 用 |
|---|---|
| 業務錯誤 / I/O 錯誤 | return error |
| 程式設計錯誤 / 不變式違反 | panic |
| 套件對外定義常見錯誤 | sentinel (var ErrXxx = errors.New(...)) |
| 錯誤要帶資料 | 自訂 struct 實作 error |
| 包裝下層錯誤 | fmt.Errorf("...: %w", err) |
| 檢查特定錯誤 | errors.Is |
| 取出特定型別 | errors.As |
| 攔截 panic | defer func(){ recover() }() |
相關閱讀
- Go 型別與介面 — 自訂 error 用到的介面知識
- Go Context — Context 取消也透過 error 傳遞(
context.Canceled、context.DeadlineExceeded) - Go 測試與基準 — 怎麼測試錯誤路徑
- Go Goroutines 完全指南 — goroutine panic 隔離
參考資源
- 官方 errors 套件:https://pkg.go.dev/errors
- Go 1.13 Error Wrapping:https://go.dev/blog/go1.13-errors
- Effective Go - Errors:https://go.dev/doc/effective_go#errors
- Working with Errors (Dave Cheney):https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
建立日期:2026-05-16 最後更新:2026-05-16