本文 AI 產出,尚未審核

Golang 錯誤處理最佳實踐

簡介

在 Go 語言中,錯誤 (error) 並不是例外 (exception) 的形式,而是 普通的值。這種設計讓錯誤的傳遞變得透明、可預測,也鼓勵開發者在每一次可能失敗的操作之後 主動檢查 錯誤。對於剛踏入 Go 世界的初學者,或是已經有其他語言背景的開發者,正確掌握錯誤處理的模式是撰寫可靠、可維護程式的關鍵。

本篇文章將從 核心概念實用範例常見陷阱最佳實踐 以及 實際應用場景 四個層面,完整說明在 Go 中如何以簡潔且安全的方式處理錯誤,並提供可直接套用的程式碼範例,幫助你在日常開發中養成良好的錯誤處理習慣。


核心概念

1. error 是一個介面

Go 標準庫定義了 error 介面:

type error interface {
    Error() string
}

任何實作了 Error() string 方法的型別,都可以當作 error 使用。最常見的做法是直接回傳 errors.Newfmt.Errorf 產生的錯誤值。

2. 多值回傳的慣例

Go 函式常見的簽名會把 結果錯誤 以兩個返回值的形式同時回傳,呼叫端必須檢查錯誤是否為 nil

func ReadFile(path string) ([]byte, error)

重點:永遠在使用結果之前先檢查錯誤,切勿假設錯誤一定是 nil

3. 錯誤的包裝與解包

自 Go 1.13 起,errors 套件提供了 errors.Iserrors.Asfmt.Errorf("%w") 等工具,讓我們可以 包裝(wrap)錯誤,同時在上層仍能判斷原始錯誤類型。

if err := doSomething(); err != nil {
    // 包裝錯誤,保留原始錯誤資訊
    return fmt.Errorf("doSomething failed: %w", err)
}

在呼叫端:

if errors.Is(err, os.ErrNotExist) {
    // 針對檔案不存在的情況做特別處理
}

4. 自訂錯誤類型

在大型專案中,僅靠字串比對會變得脆弱。自訂錯誤結構可以攜帶更多上下文資訊,並且透過 errors.As 直接取得型別。

type ValidationError struct {
    Field string
    Msg   string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Msg)
}

使用方式:

if err := validate(input); err != nil {
    var ve *ValidationError
    if errors.As(err, &ve) {
        fmt.Printf("欄位 %s 錯誤: %s\n", ve.Field, ve.Msg)
    }
    return err
}

程式碼範例

以下提供 五個 常見且實用的錯誤處理範例,從基礎到進階逐步說明。

範例 1:最基本的錯誤檢查

package main

import (
    "fmt"
    "os"
)

func readFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path) // 可能返回錯誤
    if err != nil {
        // 直接回傳錯誤,讓呼叫端決定如何處理
        return nil, err
    }
    return data, nil
}

func main() {
    content, err := readFile("config.json")
    if err != nil {
        fmt.Printf("讀取檔案失敗: %v\n", err)
        return
    }
    fmt.Println("檔案內容:", string(content))
}

說明os.ReadFile 失敗時會返回非 nil 的錯誤,我們立即檢查並回傳。這是所有 Go 程式的基本模式。

範例 2:錯誤包裝與上層判斷

package main

import (
    "errors"
    "fmt"
    "io"
    "os"
)

func openConfig(path string) (*os.File, error) {
    f, err := os.Open(path)
    if err != nil {
        // 使用 %w 包裝錯誤,保留原始類型
        return nil, fmt.Errorf("open config %s: %w", path, err)
    }
    return f, nil
}

func main() {
    _, err := openConfig("missing.yaml")
    if err != nil {
        // 判斷是否因為檔案不存在
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("設定檔不存在,使用預設值")
        } else {
            fmt.Printf("未知錯誤: %v\n", err)
        }
    }
}

說明:透過 fmt.Errorf("%w") 包裝錯誤,使得上層仍能使用 errors.Is 判斷根本原因。

範例 3:自訂錯誤類型與上下文資訊

package main

import (
    "errors"
    "fmt"
)

type NotFoundError struct {
    Resource string
    ID       int
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}

func getUser(id int) (string, error) {
    // 假設資料庫查不到
    return "", &NotFoundError{Resource: "User", ID: id}
}

func main() {
    name, err := getUser(42)
    if err != nil {
        var nf *NotFoundError
        if errors.As(err, &nf) {
            fmt.Printf("找不到資源: %s\n", nf.Error())
            // 可以根據 nf.Resource、nf.ID 做進一步處理
        } else {
            fmt.Printf("其他錯誤: %v\n", err)
        }
        return
    }
    fmt.Println("使用者名稱:", name)
}

說明:自訂錯誤讓錯誤資訊更結構化,errors.As 能直接取得錯誤的具體型別,避免字串比對的脆弱性。

範例 4:多層錯誤包裝 + 堆疊資訊

package main

import (
    "errors"
    "fmt"
    "runtime"
)

func stackTrace(err error) string {
    // 取得呼叫堆疊 (簡易版)
    pcs := make([]uintptr, 10)
    n := runtime.Callers(2, pcs)
    frames := runtime.CallersFrames(pcs[:n])

    trace := err.Error() + "\nStack trace:\n"
    for {
        frame, more := frames.Next()
        trace += fmt.Sprintf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
        if !more {
            break
        }
    }
    return trace
}

func layer3() error {
    return errors.New("底層錯誤")
}

func layer2() error {
    if err := layer3(); err != nil {
        return fmt.Errorf("layer2 failed: %w", err)
    }
    return nil
}

func layer1() error {
    if err := layer2(); err != nil {
        return fmt.Errorf("layer1 failed: %w", err)
    }
    return nil
}

func main() {
    if err := layer1(); err != nil {
        fmt.Println(stackTrace(err))
    }
}

說明%w 讓錯誤保持鏈結,stackTrace 函式示範如何在開發或除錯時把堆疊資訊印出,對於追蹤複雜流程非常有幫助。

範例 5:在測試中驗證錯誤類型

package main

import (
    "errors"
    "testing"
)

func TestGetUser_NotFound(t *testing.T) {
    _, err := getUser(999) // 這裡使用前面範例的 getUser
    if err == nil {
        t.Fatalf("預期會回傳錯誤,但得到 nil")
    }

    var nf *NotFoundError
    if !errors.As(err, &nf) {
        t.Fatalf("錯誤類型不正確,期望 *NotFoundError,實際 %T", err)
    }

    if nf.ID != 999 {
        t.Fatalf("錯誤中的 ID 應該是 999,實際 %d", nf.ID)
    }
}

說明:測試時使用 errors.As 可以精確驗證回傳的錯誤型別與內容,避免僅靠字串比對產生的誤判。


常見陷阱與最佳實踐

常見陷阱 為什麼會發生 解決方案
忘記檢查錯誤 習慣了例外拋出,忽略了 if err != nil 強制在 IDE/linters 加入 errcheck,或使用 go vet
直接回傳 fmt.Errorf 的字串 失去錯誤類型資訊,無法用 errors.Is/As 判斷 使用 %w 包裝,或自訂錯誤結構
在 defer 中忽略錯誤 defer 常用於資源釋放,錯誤被吞掉 defer記錄或回傳 錯誤,例如 defer func(){ if cerr := f.Close(); cerr != nil { log... } }()
過度包裝錯誤 包裝層級過深,堆疊資訊難以閱讀 只在邊界層(API、服務入口)包裝,內部層級保留原始錯誤
使用 panic 代替錯誤 panic 會導致程式非預期退出,難以恢復 僅在不可恢復的情況(如初始化失敗)使用 panic,其餘情況以錯誤回傳

最佳實踐總結

  1. 每一次可能失敗的呼叫,都必須檢查錯誤
  2. 在公共 API(函式、方法)返回錯誤時,使用 error 介面,不要自行定義 bool + error
  3. 使用 fmt.Errorf("%w") 包裝錯誤,保留原始錯誤類型以便上層判斷。
  4. 自訂錯誤類型(結構體)來攜帶額外上下文,配合 errors.As 使用。
  5. 在測試中驗證錯誤,確保錯誤類型與訊息符合預期。
  6. 利用 linters (errcheck, staticcheck)go vet 早期捕捉遺漏的錯誤檢查。

實際應用場景

1. Web API 的錯誤回傳

在 HTTP 伺服器中,我們常需要把內部錯誤轉換成適當的 HTTP 狀態碼與錯誤訊息:

func handler(w http.ResponseWriter, r *http.Request) {
    data, err := service.DoSomething()
    if err != nil {
        var ve *ValidationError
        if errors.As(err, &ve) {
            http.Error(w, ve.Error(), http.StatusBadRequest)
            return
        }
        // 其他錯誤視為 500
        http.Error(w, "internal server error", http.StatusInternalServerError)
        log.Printf("handler error: %v", err) // 記錄完整錯誤
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(data)
}

2. 資料庫操作的錯誤分層

func GetUserByID(id int) (*User, error) {
    row := db.QueryRow("SELECT id, name FROM users WHERE id=?", id)
    var u User
    if err := row.Scan(&u.ID, &u.Name); err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, &NotFoundError{Resource: "User", ID: id}
        }
        return nil, fmt.Errorf("query user failed: %w", err)
    }
    return &u, nil
}

上層服務只需要關心 NotFoundError,而不必了解底層的 sql.ErrNoRows

3. 多服務串接(微服務)

在微服務環境下,錯誤往往需要跨網路傳遞。使用 gRPC 時,status 包提供了錯誤碼與訊息的標準化:

import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"

func (s *server) CreateOrder(ctx context.Context, req *pb.OrderRequest) (*pb.OrderResponse, error) {
    if err := validate(req); err != nil {
        return nil, status.Error(codes.InvalidArgument, err.Error())
    }
    // 其他邏輯...
    return &pb.OrderResponse{Id: orderID}, nil
}

客戶端可以直接透過 status.Code(err) 判斷錯誤類型,而不必解析字串。


總結

錯誤處理是 Go 程式設計的核心之一,正確的模式 能讓程式碼保持 簡潔、可讀且易於除錯。本文從 error 介面的基礎概念、錯誤包裝與自訂類型、實務範例、常見陷阱到最佳實踐,提供了一套完整的思考框架與可直接套用的程式碼。

關鍵要點

  • 每一次可能失敗的呼叫,都必須檢查 err != nil
  • 使用 %w 包裝錯誤,保留原始類型,讓上層可以透過 errors.Is/As 判斷。
  • 為複雜情境自訂錯誤結構,攜帶額外上下文,提升錯誤的可診斷性。
  • 在測試、日誌、API 回傳等層面,保持錯誤資訊的一致與可追蹤。

掌握這些最佳實踐後,你的 Go 專案將能更快速定位問題、降低因未處理錯誤而產生的不可預期行為,進而提升整體開發效率與系統可靠度。祝你在 Golang 的錯誤處理之路上越走越順!