Golang 錯誤處理最佳實踐
簡介
在 Go 語言中,錯誤 (error) 並不是例外 (exception) 的形式,而是 普通的值。這種設計讓錯誤的傳遞變得透明、可預測,也鼓勵開發者在每一次可能失敗的操作之後 主動檢查 錯誤。對於剛踏入 Go 世界的初學者,或是已經有其他語言背景的開發者,正確掌握錯誤處理的模式是撰寫可靠、可維護程式的關鍵。
本篇文章將從 核心概念、實用範例、常見陷阱、最佳實踐 以及 實際應用場景 四個層面,完整說明在 Go 中如何以簡潔且安全的方式處理錯誤,並提供可直接套用的程式碼範例,幫助你在日常開發中養成良好的錯誤處理習慣。
核心概念
1. error 是一個介面
Go 標準庫定義了 error 介面:
type error interface {
Error() string
}
任何實作了 Error() string 方法的型別,都可以當作 error 使用。最常見的做法是直接回傳 errors.New 或 fmt.Errorf 產生的錯誤值。
2. 多值回傳的慣例
Go 函式常見的簽名會把 結果 與 錯誤 以兩個返回值的形式同時回傳,呼叫端必須檢查錯誤是否為 nil:
func ReadFile(path string) ([]byte, error)
重點:永遠在使用結果之前先檢查錯誤,切勿假設錯誤一定是
nil。
3. 錯誤的包裝與解包
自 Go 1.13 起,errors 套件提供了 errors.Is、errors.As、fmt.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,其餘情況以錯誤回傳 |
最佳實踐總結
- 每一次可能失敗的呼叫,都必須檢查錯誤。
- 在公共 API(函式、方法)返回錯誤時,使用
error介面,不要自行定義bool+error。 - 使用
fmt.Errorf("%w")包裝錯誤,保留原始錯誤類型以便上層判斷。 - 自訂錯誤類型(結構體)來攜帶額外上下文,配合
errors.As使用。 - 在測試中驗證錯誤,確保錯誤類型與訊息符合預期。
- 利用 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 的錯誤處理之路上越走越順!