Go 錯誤處理進階
目錄
- Go 錯誤處理哲學
- error 接口本質
- 自定義錯誤類型
- fmt.Errorf 與 %w 包裝錯誤
- errors.Is 和 errors.As
- 哨兵錯誤模式
- 錯誤處理最佳實踐
- 實際項目中的錯誤處理模式
1. Go 錯誤處理哲學
1.1 與 try-catch 的根本區別
在 Java、Python、C++ 等語言中,異常處理依賴 try-catch 機制——異常被"拋出"後沿調用棧向上傳播,直到被某個 catch 塊捕獲。這種模式有幾個問題:
- 異常的傳播路徑是隱式的,調用者不知道某個函數可能拋出什麼異常
- 異常控制流類似
goto,會打斷正常的代碼邏輯 - 容易被濫用(用異常做流程控制)
Go 採用了完全不同的哲學:錯誤是值(errors are values)。錯誤通過函數返回值顯式傳遞,調用者必須主動檢查並處理錯誤。
// Java 風格(隱式異常傳播)
try {
File f = openFile("config.json");
String data = readAll(f);
Config config = parse(data);
} catch (FileNotFoundException e) {
// 處理文件不存在
} catch (IOException e) {
// 處理IO錯誤
} catch (ParseException e) {
// 處理解析錯誤
}
// Go 風格(顯式錯誤處理)
f, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("打開配置文件失敗: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("讀取配置文件失敗: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("解析配置文件失敗: %w", err)
}
1.2 顯式錯誤處理的優勢
package main
import (
"fmt"
"strconv"
)
// 每個可能失敗的操作都顯式返回錯誤
// 調用者可以清楚看到哪些步驟可能出錯
func parseAndDouble(s string) (int, error) {
// 第一步:解析字符串爲整數
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("解析整數失敗: %w", err)
}
// 第二步:檢查範圍
if n < 0 || n > 1000 {
return 0, fmt.Errorf("數值 %d 超出有效範圍 [0, 1000]", n)
}
return n * 2, nil
}
func main() {
// 正常情況
result, err := parseAndDouble("42")
if err != nil {
fmt.Println("錯誤:", err)
return
}
fmt.Println("結果:", result) // 結果: 84
// 非數字
_, err = parseAndDouble("abc")
if err != nil {
fmt.Println("錯誤:", err) // 錯誤: 解析整數失敗: strconv.Atoi: parsing "abc": invalid syntax
}
// 超出範圍
_, err = parseAndDouble("9999")
if err != nil {
fmt.Println("錯誤:", err) // 錯誤: 數值 9999 超出有效範圍 [0, 1000]
}
}
1.3 Go 的錯誤處理格言
Rob Pike 在 "Errors are values" 博文中指出,既然錯誤是普通的值,就可以用編程技巧來簡化錯誤處理,而不是機械地 if err != nil。
package main
import (
"bufio"
"fmt"
"os"
)
// errWriter 封裝了 writer 和錯誤狀態
// 一旦發生錯誤,後續寫入操作自動跳過
type errWriter struct {
w *bufio.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // 已經有錯誤了,跳過
}
_, ew.err = ew.w.Write(buf)
}
func (ew *errWriter) flush() error {
if ew.err != nil {
return ew.err
}
return ew.w.Flush()
}
func writeReport(f *os.File) error {
bw := bufio.NewWriter(f)
ew := &errWriter{w: bw}
// 不需要每次都檢查錯誤,最後統一檢查
ew.write([]byte("=== 報告標題 ===\n"))
ew.write([]byte("第一部分內容...\n"))
ew.write([]byte("第二部分內容...\n"))
ew.write([]byte("=== 報告結束 ===\n"))
return ew.flush()
}
func main() {
f, err := os.Create("/tmp/report.txt")
if err != nil {
fmt.Println("創建文件失敗:", err)
return
}
defer f.Close()
if err := writeReport(f); err != nil {
fmt.Println("寫入報告失敗:", err)
return
}
fmt.Println("報告寫入成功")
}
2. error 接口本質
2.1 error 是一個接口
Go 內置的 error 類型實際上是一個非常簡單的接口:
// Go 標準庫中的定義
type error interface {
Error() string
}
任何實現了 Error() string 方法的類型都滿足 error 接口。這種設計極其簡潔而強大。
2.2 標準庫中的 errors.New 和 fmt.Errorf
package main
import (
"errors"
"fmt"
)
func main() {
// errors.New 創建一個簡單的錯誤
err1 := errors.New("something went wrong")
fmt.Println(err1) // something went wrong
fmt.Printf("%T\n", err1) // *errors.errorString
// fmt.Errorf 創建格式化的錯誤
name := "config.json"
err2 := fmt.Errorf("無法打開文件 %s", name)
fmt.Println(err2) // 無法打開文件 config.json
// error 的零值是 nil,表示沒有錯誤
var err3 error
fmt.Println(err3 == nil) // true
}
2.3 errors.New 的內部實現
// 標準庫 errors 包的實現非常簡單
// 這裏展示其核心邏輯
package main
import "fmt"
// errorString 是一個實現了 error 接口的簡單結構體
type errorString struct {
s string
}
// Error 方法使 errorString 滿足 error 接口
func (e *errorString) Error() string {
return e.s
}
// New 創建一個新的錯誤,返回指針
// 返回指針而不是值,是爲了讓每次 New 調用產生不同的錯誤實例
func New(text string) error {
return &errorString{text}
}
func main() {
err1 := New("出錯了")
err2 := New("出錯了")
// 雖然文本相同,但它們是不同的錯誤實例(指針不同)
fmt.Println(err1 == err2) // false
fmt.Println(err1) // 出錯了
}
2.4 nil error 的陷阱
package main
import "fmt"
// MyError 自定義錯誤類型
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("錯誤碼 %d: %s", e.Code, e.Message)
}
func doSomething(fail bool) error {
// 注意:這裏聲明瞭一個具體類型的指針
var err *MyError
if fail {
err = &MyError{Code: 404, Message: "未找到"}
}
// 危險!即使 err 是 nil 指針,返回後 error 接口值不爲 nil
return err
}
func doSomethingCorrect(fail bool) error {
if fail {
return &MyError{Code: 404, Message: "未找到"}
}
// 直接返回 nil,不經過具體類型變量
return nil
}
func main() {
// 錯誤示範:即使沒有失敗,err 也不等於 nil!
err := doSomething(false)
if err != nil {
// 會進入這裏!因爲 error 接口內部持有 (*MyError, nil)
// 接口值只有在類型和值都爲 nil 時纔等於 nil
fmt.Println("錯誤的判斷 - 進入了錯誤分支")
fmt.Printf("err 類型: %T, 值: %v\n", err, err)
}
// 正確示範
err2 := doSomethingCorrect(false)
if err2 == nil {
fmt.Println("正確:沒有錯誤") // 正確:沒有錯誤
}
}
關鍵教訓:永遠不要返回一個具體類型的 nil 指針作爲 error 接口值。如果沒有錯誤,直接
return nil。
3. 自定義錯誤類型
3.1 簡單的自定義錯誤
package main
import "fmt"
// ValidationError 驗證錯誤,包含字段名和錯誤原因
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("驗證失敗 [%s]: %s", e.Field, e.Message)
}
// NotFoundError 資源未找到錯誤
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s (ID: %s) 不存在", e.Resource, e.ID)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "年齡不能爲負數",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Message: "年齡不能超過150",
}
}
return nil
}
func findUser(id string) (string, error) {
// 模擬查找用戶
users := map[string]string{
"1": "Alice",
"2": "Bob",
}
name, ok := users[id]
if !ok {
return "", &NotFoundError{
Resource: "用戶",
ID: id,
}
}
return name, nil
}
func main() {
// 測試驗證錯誤
if err := validateAge(-5); err != nil {
fmt.Println(err) // 驗證失敗 [age]: 年齡不能爲負數
// 類型斷言獲取詳細信息
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
}
}
// 測試未找到錯誤
_, err := findUser("99")
if err != nil {
fmt.Println(err) // 用戶 (ID: 99) 不存在
if nfe, ok := err.(*NotFoundError); ok {
fmt.Printf("資源類型: %s, ID: %s\n", nfe.Resource, nfe.ID)
}
}
}
3.2 攜帶上下文的錯誤類型
package main
import (
"fmt"
"time"
)
// OpError 記錄操作上下文的錯誤
type OpError struct {
Op string // 操作名稱
Path string // 資源路徑
Err error // 底層錯誤(支持錯誤鏈)
Time time.Time // 發生時間
}
func (e *OpError) Error() string {
return fmt.Sprintf("操作 %s 在 %s 上失敗 [%s]: %v",
e.Op, e.Path, e.Time.Format("15:04:05"), e.Err)
}
// Unwrap 支持錯誤鏈解包
func (e *OpError) Unwrap() error {
return e.Err
}
// PermissionError 權限錯誤
type PermissionError struct {
User string
Action string
}
func (e *PermissionError) Error() string {
return fmt.Sprintf("用戶 %s 沒有 %s 權限", e.User, e.Action)
}
func readConfig(user string) error {
// 模擬權限檢查失敗
permErr := &PermissionError{
User: user,
Action: "read",
}
// 包裝爲操作錯誤
return &OpError{
Op: "ReadConfig",
Path: "/etc/app/config.yaml",
Err: permErr,
Time: time.Now(),
}
}
func main() {
err := readConfig("guest")
if err != nil {
fmt.Println(err)
// 操作 ReadConfig 在 /etc/app/config.yaml 上失敗 [14:30:05]: 用戶 guest 沒有 read 權限
}
}
3.3 多錯誤聚合
package main
import (
"fmt"
"strings"
)
// MultiError 聚合多個錯誤
type MultiError struct {
Errors []error
}
func (me *MultiError) Error() string {
if len(me.Errors) == 0 {
return "沒有錯誤"
}
msgs := make([]string, len(me.Errors))
for i, err := range me.Errors {
msgs[i] = fmt.Sprintf(" [%d] %s", i+1, err.Error())
}
return fmt.Sprintf("發生 %d 個錯誤:\n%s", len(me.Errors), strings.Join(msgs, "\n"))
}
// Add 添加錯誤(忽略 nil)
func (me *MultiError) Add(err error) {
if err != nil {
me.Errors = append(me.Errors, err)
}
}
// ErrorOrNil 如果沒有錯誤則返回 nil
func (me *MultiError) ErrorOrNil() error {
if len(me.Errors) == 0 {
return nil
}
return me
}
// validateUser 同時驗證多個字段
func validateUser(name string, age int, email string) error {
me := &MultiError{}
if name == "" {
me.Add(fmt.Errorf("姓名不能爲空"))
}
if age < 0 || age > 150 {
me.Add(fmt.Errorf("年齡 %d 不在有效範圍內", age))
}
if !strings.Contains(email, "@") {
me.Add(fmt.Errorf("郵箱 %q 格式不正確", email))
}
return me.ErrorOrNil()
}
func main() {
// 多個字段驗證失敗
err := validateUser("", -1, "not-an-email")
if err != nil {
fmt.Println(err)
// 發生 3 個錯誤:
// [1] 姓名不能爲空
// [2] 年齡 -1 不在有效範圍內
// [3] 郵箱 "not-an-email" 格式不正確
}
// 全部驗證通過
err = validateUser("Alice", 30, "alice@example.com")
fmt.Println("全部通過:", err == nil) // 全部通過: true
}
4. fmt.Errorf 與 %w 包裝錯誤
4.1 錯誤包裝基礎(Go 1.13+)
Go 1.13 引入了錯誤包裝機制,通過 fmt.Errorf 的 %w 動詞可以將一個錯誤包裝在另一個錯誤中,形成錯誤鏈。
package main
import (
"errors"
"fmt"
"os"
)
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// %w 包裝原始錯誤,保留錯誤鏈
return nil, fmt.Errorf("讀取文件 %s: %w", path, err)
}
return data, nil
}
func loadConfig() ([]byte, error) {
data, err := readFile("/etc/app/config.json")
if err != nil {
// 繼續包裝,形成多層錯誤鏈
return nil, fmt.Errorf("加載配置: %w", err)
}
return data, nil
}
func main() {
_, err := loadConfig()
if err != nil {
fmt.Println("錯誤信息:", err)
// 加載配置: 讀取文件 /etc/app/config.json: open /etc/app/config.json: no such file or directory
// 可以通過 errors.Is 檢查底層錯誤
if errors.Is(err, os.ErrNotExist) {
fmt.Println("根因:文件不存在")
}
}
}
4.2 %w 與 %v 的區別
package main
import (
"errors"
"fmt"
"io"
)
func example_w() error {
// %w 包裝錯誤 -- 保留錯誤鏈,可以用 errors.Is/errors.As 查找
return fmt.Errorf("操作失敗: %w", io.EOF)
}
func example_v() error {
// %v 僅格式化錯誤文本 -- 丟失錯誤鏈,無法用 errors.Is/errors.As 查找
return fmt.Errorf("操作失敗: %v", io.EOF)
}
func main() {
err1 := example_w()
fmt.Println("使用 %%w:", errors.Is(err1, io.EOF)) // true -- 錯誤鏈完整
err2 := example_v()
fmt.Println("使用 %%v:", errors.Is(err2, io.EOF)) // false -- 錯誤鏈斷裂
}
4.3 多重包裝(Go 1.20+)
Go 1.20 起,fmt.Errorf 支持多個 %w,可以同時包裝多個錯誤:
package main
import (
"errors"
"fmt"
"io"
"os"
)
func complexOperation() error {
err1 := os.ErrPermission
err2 := io.ErrUnexpectedEOF
// 同時包裝兩個錯誤
return fmt.Errorf("操作失敗: %w 且 %w", err1, err2)
}
func main() {
err := complexOperation()
fmt.Println(err) // 操作失敗: permission denied 且 unexpected EOF
// 兩個底層錯誤都可以被 errors.Is 找到
fmt.Println("是權限錯誤?", errors.Is(err, os.ErrPermission)) // true
fmt.Println("是意外EOF?", errors.Is(err, io.ErrUnexpectedEOF)) // true
}
4.4 手動實現 Unwrap
package main
import (
"errors"
"fmt"
"io"
)
// QueryError 包裝底層錯誤的自定義類型
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("查詢 %q 失敗: %v", e.Query, e.Err)
}
// Unwrap 讓 errors.Is/errors.As 能沿鏈查找
func (e *QueryError) Unwrap() error {
return e.Err
}
// JoinedError 包裝多個底層錯誤(Go 1.20 風格)
type JoinedError struct {
Message string
Errs []error
}
func (e *JoinedError) Error() string {
return e.Message
}
// Unwrap 返回切片(Go 1.20+ 支持的多錯誤解包協議)
func (e *JoinedError) Unwrap() []error {
return e.Errs
}
func main() {
// 單錯誤鏈
qErr := &QueryError{
Query: "SELECT * FROM users",
Err: io.EOF,
}
fmt.Println(qErr) // 查詢 "SELECT * FROM users" 失敗: EOF
fmt.Println(errors.Is(qErr, io.EOF)) // true
// 多錯誤鏈
jErr := &JoinedError{
Message: "多個操作失敗",
Errs: []error{io.EOF, io.ErrClosedPipe},
}
fmt.Println(errors.Is(jErr, io.EOF)) // true
fmt.Println(errors.Is(jErr, io.ErrClosedPipe)) // true
}
5. errors.Is 和 errors.As
5.1 errors.Is -- 檢查錯誤鏈中是否包含某個特定錯誤值
package main
import (
"errors"
"fmt"
"io"
"os"
)
var ErrDatabase = errors.New("數據庫錯誤")
func queryDB() error {
return fmt.Errorf("執行查詢: %w", ErrDatabase)
}
func handleRequest() error {
return fmt.Errorf("處理請求: %w", queryDB())
}
func main() {
err := handleRequest()
// errors.Is 會沿著錯誤鏈逐層查找
// err -> "處理請求" -> "執行查詢" -> ErrDatabase
fmt.Println(errors.Is(err, ErrDatabase)) // true
// 常見的標準庫哨兵錯誤判斷
err2 := fmt.Errorf("讀取結束: %w", io.EOF)
fmt.Println(errors.Is(err2, io.EOF)) // true
// 不在錯誤鏈中的錯誤
fmt.Println(errors.Is(err, os.ErrNotExist)) // false
// == 只能匹配同一個錯誤實例,不能沿鏈查找
fmt.Println(err == ErrDatabase) // false(err 是包裝後的,不等於原始值)
}
5.2 errors.As -- 從錯誤鏈中提取特定類型的錯誤
package main
import (
"errors"
"fmt"
"net"
)
// APIError 自定義API錯誤
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API錯誤 %d: %s", e.StatusCode, e.Message)
}
func callAPI() error {
apiErr := &APIError{StatusCode: 403, Message: "禁止訪問"}
return fmt.Errorf("調用用戶服務: %w", apiErr)
}
func main() {
err := callAPI()
// errors.As 從錯誤鏈中找到匹配類型的錯誤並賦值給 target
var apiErr *APIError
if errors.As(err, &apiErr) {
fmt.Printf("狀態碼: %d, 消息: %s\n", apiErr.StatusCode, apiErr.Message)
// 狀態碼: 403, 消息: 禁止訪問
}
// 標準庫中的實際應用:提取網絡錯誤的詳細信息
_, netErr := net.Dial("tcp", "invalid-host:99999")
if netErr != nil {
var opErr *net.OpError
if errors.As(netErr, &opErr) {
fmt.Printf("網絡操作: %s, 地址: %v\n", opErr.Op, opErr.Addr)
}
}
}
5.3 errors.Is 與 errors.As 的對比
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 製造一個文件操作錯誤
_, err := os.Open("/這個文件不存在")
if err == nil {
return
}
wrappedErr := fmt.Errorf("初始化失敗: %w", err)
// errors.Is: 用於匹配特定的錯誤【值】
// 問題:這個錯誤是不是 os.ErrNotExist?
if errors.Is(wrappedErr, os.ErrNotExist) {
fmt.Println("Is: 文件不存在") // 匹配成功
}
// errors.As: 用於匹配特定的錯誤【類型】,並提取該類型的值
// 問題:這個錯誤鏈中有沒有 *os.PathError 類型的錯誤?
var pathErr *os.PathError
if errors.As(wrappedErr, &pathErr) {
fmt.Printf("As: 路徑=%s, 操作=%s\n", pathErr.Path, pathErr.Op)
// As: 路徑=/這個文件不存在, 操作=open
}
// 總結:
// errors.Is(err, target) -> 值比較,類似 err == target 但支持錯誤鏈
// errors.As(err, &target) -> 類型匹配,類似類型斷言但支持錯誤鏈
}
5.4 自定義 Is 和 As 方法
package main
import (
"errors"
"fmt"
)
// ErrCode 基於錯誤碼的錯誤類型
type ErrCode struct {
Code int
Message string
}
func (e *ErrCode) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Is 自定義比較邏輯:只要錯誤碼相同就認爲匹配
func (e *ErrCode) Is(target error) bool {
t, ok := target.(*ErrCode)
if !ok {
return false
}
return e.Code == t.Code
}
func main() {
err1 := &ErrCode{Code: 404, Message: "用戶未找到"}
err2 := &ErrCode{Code: 404, Message: "訂單未找到"}
err3 := &ErrCode{Code: 500, Message: "內部錯誤"}
// 雖然消息不同,但錯誤碼相同,Is 返回 true
fmt.Println(errors.Is(err1, err2)) // true
fmt.Println(errors.Is(err1, err3)) // false
// 包裝後仍然有效
wrapped := fmt.Errorf("服務層: %w", err1)
target := &ErrCode{Code: 404} // 只關心錯誤碼
fmt.Println(errors.Is(wrapped, target)) // true
}
6. 哨兵錯誤模式
6.1 什麼是哨兵錯誤
哨兵錯誤(Sentinel Errors)是預定義的、包級別的錯誤變量,用作特定條件的標識符。調用者通過 errors.Is 來檢查是否遇到了某個特定的已知錯誤。
package main
import (
"errors"
"fmt"
"io"
"bufio"
"strings"
)
func main() {
// 標準庫中最常見的哨兵錯誤:io.EOF
reader := strings.NewReader("Hello")
scanner := bufio.NewScanner(reader)
// io.EOF 表示數據讀取完畢,不是真正的"錯誤"
buf := make([]byte, 10)
for {
n, err := strings.NewReader("Hello").Read(buf)
if errors.Is(err, io.EOF) {
fmt.Printf("讀取完畢,最後讀取 %d 字節\n", n)
break
}
if err != nil {
fmt.Println("讀取出錯:", err)
break
}
fmt.Printf("讀取 %d 字節: %s\n", n, buf[:n])
}
_ = scanner
}
6.2 標準庫中的哨兵錯誤示例
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"time"
)
func main() {
// 1. io.EOF - 數據流結束
fmt.Println("io.EOF:", io.EOF)
// 2. io.ErrUnexpectedEOF - 讀取到一半意外終止
fmt.Println("io.ErrUnexpectedEOF:", io.ErrUnexpectedEOF)
// 3. os.ErrNotExist - 文件不存在
_, err := os.Stat("/不存在的文件")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
// 4. os.ErrPermission - 權限不足
fmt.Println("os.ErrPermission:", os.ErrPermission)
// 5. sql.ErrNoRows - 查詢沒有返回結果
// 注意:這不是錯誤,只是表示沒有匹配的行
fmt.Println("sql.ErrNoRows:", sql.ErrNoRows)
// 6. context.Canceled - 上下文被取消
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println("ctx.Err():", ctx.Err())
fmt.Println("是 Canceled?", errors.Is(ctx.Err(), context.Canceled))
// 7. context.DeadlineExceeded - 上下文超時
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel2()
time.Sleep(1 * time.Millisecond)
fmt.Println("是 DeadlineExceeded?", errors.Is(ctx2.Err(), context.DeadlineExceeded))
}
6.3 自定義哨兵錯誤
package main
import (
"errors"
"fmt"
)
// 包級別的哨兵錯誤,使用 errors.New 創建
var (
ErrUserNotFound = errors.New("用戶未找到")
ErrUserExists = errors.New("用戶已存在")
ErrInvalidPassword = errors.New("密碼無效")
ErrAccountLocked = errors.New("賬戶已鎖定")
)
// UserStore 模擬用戶存儲
type UserStore struct {
users map[string]string // username -> password
}
func NewUserStore() *UserStore {
return &UserStore{
users: map[string]string{
"alice": "secret123",
},
}
}
func (s *UserStore) Register(username, password string) error {
if _, exists := s.users[username]; exists {
return fmt.Errorf("註冊用戶 %s: %w", username, ErrUserExists)
}
s.users[username] = password
return nil
}
func (s *UserStore) Login(username, password string) error {
stored, exists := s.users[username]
if !exists {
return fmt.Errorf("登錄 %s: %w", username, ErrUserNotFound)
}
if stored != password {
return fmt.Errorf("登錄 %s: %w", username, ErrInvalidPassword)
}
return nil
}
func main() {
store := NewUserStore()
// 註冊已存在的用戶
err := store.Register("alice", "newpass")
if errors.Is(err, ErrUserExists) {
fmt.Println("註冊失敗:用戶已存在")
}
// 登錄不存在的用戶
err = store.Login("bob", "pass")
if errors.Is(err, ErrUserNotFound) {
fmt.Println("登錄失敗:用戶未找到")
}
// 密碼錯誤
err = store.Login("alice", "wrong")
if errors.Is(err, ErrInvalidPassword) {
fmt.Println("登錄失敗:密碼錯誤")
}
// 使用 switch 分發錯誤處理
err = store.Login("bob", "test")
switch {
case errors.Is(err, ErrUserNotFound):
fmt.Println("-> 建議註冊新賬號")
case errors.Is(err, ErrInvalidPassword):
fmt.Println("-> 建議重置密碼")
case errors.Is(err, ErrAccountLocked):
fmt.Println("-> 請聯繫管理員")
case err != nil:
fmt.Println("-> 未知錯誤:", err)
default:
fmt.Println("-> 登錄成功")
}
}
7. 錯誤處理最佳實踐
7.1 何時使用 panic
panic 用於不可恢復的程序錯誤,而不是業務邏輯錯誤。
package main
import "fmt"
// 合理使用 panic 的場景
// 1. 程序初始化時的致命錯誤
func mustConnect(dsn string) {
// 如果數據庫連接失敗,程序無法運行
if dsn == "" {
panic("數據庫連接字符串不能爲空")
}
// ... 連接數據庫
}
// 2. Must 函數模式:將 error 轉化爲 panic
// 標準庫中有 template.Must, regexp.MustCompile 等
func MustParseConfig(path string) map[string]string {
config := map[string]string{"key": "value"} // 模擬解析
if path == "" {
panic("配置文件路徑爲空")
}
return config
}
// 3. 真正不可能發生的錯誤(程序邏輯bug)
func divide(a, b int) int {
if b == 0 {
// 如果調用者保證 b != 0,這裏 panic 表示程序有 bug
panic("除數爲零:這是一個編程錯誤")
}
return a / b
}
func main() {
// Must 模式通常在 init 或 main 的開頭使用
config := MustParseConfig("app.yaml")
fmt.Println(config)
}
7.2 recover 捕獲 panic
package main
import "fmt"
// safeExecute 安全執行一個函數,捕獲可能的 panic
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
// 將 panic 轉化爲 error
err = fmt.Errorf("捕獲到 panic: %v", r)
}
}()
fn()
return nil
}
func riskyFunction() {
panic("出大事了!")
}
func main() {
// 場景:HTTP 中間件防止單個請求的 panic 導致服務崩潰
err := safeExecute(riskyFunction)
if err != nil {
fmt.Println("安全捕獲:", err)
// 安全捕獲: 捕獲到 panic: 出大事了!
}
fmt.Println("程序繼續運行...")
}
7.3 錯誤處理的黃金法則
package main
import (
"fmt"
"log"
)
// 規則1: 錯誤只應該被處理一次
// 要麼處理它(記錄日誌、降級、返回默認值),要麼向上傳遞
// 不要既記日誌又返回錯誤
// 錯誤示範
func badExample() error {
err := fmt.Errorf("something failed")
if err != nil {
log.Println("錯誤:", err) // 處理了一次(記日誌)
return err // 又返回了(讓調用者再處理一次)
}
return nil
}
// 正確示範 A: 只向上傳遞(加上下文)
func goodExampleA() error {
err := fmt.Errorf("something failed")
if err != nil {
return fmt.Errorf("執行操作X: %w", err) // 只傳遞,不記日誌
}
return nil
}
// 正確示範 B: 只在這裏處理
func goodExampleB() int {
err := fmt.Errorf("something failed")
if err != nil {
log.Println("操作X失敗,使用默認值:", err) // 處理:記日誌+降級
return 42 // 返回默認值
}
return 0
}
// 規則2: 總是爲錯誤添加上下文
func fetchUserOrders(userID string) error {
// 不好: return err
// 好: return fmt.Errorf("獲取用戶 %s 的訂單: %w", userID, err)
return fmt.Errorf("獲取用戶 %s 的訂單: %w", userID,
fmt.Errorf("數據庫查詢超時"))
}
// 規則3: 在頂層處理錯誤,底層只傳遞
func main() {
// 這裏是"頂層",負責最終處理
if err := fetchUserOrders("u123"); err != nil {
log.Println("請求處理失敗:", err)
// 可能還要返回 HTTP 500、發送告警等
}
_ = badExample()
_ = goodExampleA()
_ = goodExampleB()
}
7.4 不要忽略錯誤
package main
import (
"fmt"
"os"
)
func main() {
// 糟糕:忽略了 Close 的錯誤(寫入可能沒有刷到磁盤)
f, _ := os.Create("/tmp/test.txt")
f.Write([]byte("data"))
f.Close() // Close 可能返回錯誤!
// 正確方式
f2, err := os.Create("/tmp/test2.txt")
if err != nil {
fmt.Println("創建文件失敗:", err)
return
}
_, err = f2.Write([]byte("data"))
if err != nil {
f2.Close()
fmt.Println("寫入失敗:", err)
return
}
if err := f2.Close(); err != nil {
fmt.Println("關閉文件失敗(數據可能未完全寫入):", err)
return
}
// 如果確實要忽略某個錯誤,用 _ 顯式忽略並加註釋
_ = os.Remove("/tmp/maybe-not-exists.txt") // 忽略: 文件可能不存在,不影響邏輯
}
8. 實際項目中的錯誤處理模式
8.1 分層錯誤設計
在一個分層架構中(Repository -> Service -> Handler),每層添加自己的上下文:
package main
import (
"errors"
"fmt"
)
// ========== 基礎錯誤定義 ==========
var (
ErrNotFound = errors.New("資源未找到")
ErrUnauthorized = errors.New("未授權")
ErrInternal = errors.New("內部錯誤")
)
// ========== Repository 層 ==========
type UserRepo struct{}
func (r *UserRepo) FindByID(id int) (string, error) {
if id == 0 {
return "", fmt.Errorf("UserRepo.FindByID(id=%d): %w", id, ErrNotFound)
}
if id < 0 {
return "", fmt.Errorf("UserRepo.FindByID(id=%d): 數據庫連接失敗: %w", id, ErrInternal)
}
return "Alice", nil
}
// ========== Service 層 ==========
type UserService struct {
repo *UserRepo
}
func (s *UserService) GetUserProfile(id int) (string, error) {
name, err := s.repo.FindByID(id)
if err != nil {
return "", fmt.Errorf("UserService.GetUserProfile: %w", err)
}
return fmt.Sprintf("用戶資料: %s", name), nil
}
// ========== Handler 層 ==========
type UserHandler struct {
service *UserService
}
func (h *UserHandler) HandleGetUser(id int) {
profile, err := h.service.GetUserProfile(id)
if err != nil {
// 在最頂層根據錯誤類型決定如何響應
switch {
case errors.Is(err, ErrNotFound):
fmt.Printf("HTTP 404: %v\n", err)
case errors.Is(err, ErrUnauthorized):
fmt.Printf("HTTP 401: %v\n", err)
case errors.Is(err, ErrInternal):
fmt.Printf("HTTP 500: %v\n", err)
default:
fmt.Printf("HTTP 500: 未知錯誤: %v\n", err)
}
return
}
fmt.Printf("HTTP 200: %s\n", profile)
}
func main() {
handler := &UserHandler{
service: &UserService{
repo: &UserRepo{},
},
}
handler.HandleGetUser(1) // HTTP 200: 用戶資料: Alice
handler.HandleGetUser(0) // HTTP 404: UserService.GetUserProfile: UserRepo.FindByID(id=0): 資源未找到
handler.HandleGetUser(-1) // HTTP 500: UserService.GetUserProfile: UserRepo.FindByID(id=-1): 數據庫連接失敗: 內部錯誤
}
8.2 錯誤碼設計模式
package main
import (
"errors"
"fmt"
)
// AppError 統一的應用錯誤類型
type AppError struct {
Code string // 業務錯誤碼,如 "USER_001"
Message string // 面向用戶的消息
Err error // 底層錯誤(可選)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// Is 允許通過錯誤碼進行匹配
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// HTTPStatus 根據錯誤碼前綴返回 HTTP 狀態碼
func (e *AppError) HTTPStatus() int {
switch e.Code[:3] {
case "VAL": // 驗證錯誤
return 400
case "AUT": // 認證錯誤
return 401
case "FBD": // 權限錯誤
return 403
case "NTF": // 未找到
return 404
default:
return 500
}
}
// 預定義的錯誤模板
var (
ErrCodeValidation = &AppError{Code: "VAL_001"}
ErrCodeNotFound = &AppError{Code: "NTF_001"}
ErrCodeAuth = &AppError{Code: "AUT_001"}
ErrCodeInternal = &AppError{Code: "INT_001"}
)
// NewNotFoundError 創建一個未找到錯誤
func NewNotFoundError(resource string, id interface{}) *AppError {
return &AppError{
Code: "NTF_001",
Message: fmt.Sprintf("%s (ID=%v) 不存在", resource, id),
}
}
// NewValidationError 創建一個驗證錯誤
func NewValidationError(field, reason string) *AppError {
return &AppError{
Code: "VAL_001",
Message: fmt.Sprintf("字段 %s 驗證失敗: %s", field, reason),
}
}
// NewInternalError 創建一個內部錯誤(包裝底層錯誤)
func NewInternalError(msg string, cause error) *AppError {
return &AppError{
Code: "INT_001",
Message: msg,
Err: cause,
}
}
func processOrder(userID, productID int) error {
if userID <= 0 {
return NewValidationError("userID", "必須大於0")
}
if productID == 99 {
return NewNotFoundError("商品", productID)
}
if productID < 0 {
return NewInternalError("查詢商品失敗",
fmt.Errorf("數據庫超時"))
}
return nil
}
func handleRequest(userID, productID int) {
err := processOrder(userID, productID)
if err == nil {
fmt.Println("200 OK: 訂單創建成功")
return
}
// 從錯誤中提取 AppError
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("HTTP %d | 錯誤碼: %s | %s\n",
appErr.HTTPStatus(), appErr.Code, appErr.Message)
} else {
fmt.Println("HTTP 500 | 未知錯誤:", err)
}
}
func main() {
handleRequest(1, 1) // 200 OK: 訂單創建成功
handleRequest(0, 1) // HTTP 400 | 錯誤碼: VAL_001 | 字段 userID 驗證失敗: 必須大於0
handleRequest(1, 99) // HTTP 404 | 錯誤碼: NTF_001 | 商品 (ID=99) 不存在
handleRequest(1, -1) // HTTP 500 | 錯誤碼: INT_001 | 查詢商品失敗
// 使用 errors.Is 通過錯誤碼模板匹配
err := processOrder(0, 1)
fmt.Println("\n是驗證錯誤?", errors.Is(err, ErrCodeValidation)) // true
fmt.Println("是未找到?", errors.Is(err, ErrCodeNotFound)) // false
}
8.3 defer 與錯誤處理的結合
package main
import (
"fmt"
"os"
)
// writeToFile 演示使用 defer 和命名返回值處理關閉錯誤
func writeToFile(path, content string) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("創建文件: %w", err)
}
// 使用 defer + 命名返回值來處理 Close 錯誤
defer func() {
closeErr := f.Close()
if err == nil {
// 如果寫入沒有出錯,但 Close 出錯了,返回 Close 的錯誤
err = closeErr
}
// 如果寫入已經出錯了,Close 的錯誤就忽略(保留原始錯誤)
}()
_, err = f.WriteString(content)
if err != nil {
return fmt.Errorf("寫入內容: %w", err)
}
return nil
}
// transaction 模擬數據庫事務的錯誤處理模式
func transaction(shouldFail bool) (err error) {
fmt.Println("開始事務")
defer func() {
if err != nil {
fmt.Println("回滾事務")
// rollback()
} else {
fmt.Println("提交事務")
// commit()
}
}()
fmt.Println("執行操作1")
if shouldFail {
return fmt.Errorf("操作1失敗")
}
fmt.Println("執行操作2")
return nil
}
func main() {
// 寫文件
if err := writeToFile("/tmp/test_defer.txt", "Hello, Go!"); err != nil {
fmt.Println("寫文件失敗:", err)
} else {
fmt.Println("寫文件成功")
}
// 事務成功
fmt.Println("\n--- 事務成功 ---")
transaction(false)
// 事務失敗
fmt.Println("\n--- 事務失敗 ---")
transaction(true)
}
8.4 使用 errors.Join(Go 1.20+)
package main
import (
"errors"
"fmt"
"os"
)
// cleanup 清理多個資源,收集所有錯誤
func cleanup(files []*os.File) error {
var errs []error
for _, f := range files {
if err := f.Close(); err != nil {
errs = append(errs, fmt.Errorf("關閉 %s: %w", f.Name(), err))
}
}
// errors.Join 將多個錯誤合併爲一個
// 如果 errs 爲空,返回 nil
return errors.Join(errs...)
}
func main() {
// errors.Join 基本用法
err := errors.Join(
fmt.Errorf("錯誤1: 連接超時"),
fmt.Errorf("錯誤2: 寫入失敗"),
nil, // nil 會被自動忽略
fmt.Errorf("錯誤3: 緩存未命中"),
)
fmt.Println("合併後的錯誤:")
fmt.Println(err)
// 錯誤1: 連接超時
// 錯誤2: 寫入失敗
// 錯誤3: 緩存未命中
// errors.Join 返回的錯誤也支持 errors.Is
sentinel := errors.New("特殊錯誤")
joined := errors.Join(
fmt.Errorf("包裝: %w", sentinel),
fmt.Errorf("另一個錯誤"),
)
fmt.Println("\n包含特殊錯誤?", errors.Is(joined, sentinel)) // true
}
總結
| 場景 | 推薦方式 |
|---|---|
| 函數可能失敗 | 返回 (result, error) |
| 添加上下文 | fmt.Errorf("上下文: %w", err) |
| 檢查特定錯誤值 | errors.Is(err, target) |
| 提取特定錯誤類型 | errors.As(err, &target) |
| 定義已知錯誤條件 | 包級別哨兵變量 var ErrXxx = errors.New(...) |
| 定義攜帶數據的錯誤 | 自定義結構體實現 error 接口 |
| 不可恢復的錯誤 | panic(僅限程序 bug 或初始化失敗) |
| 合併多個錯誤 | errors.Join(Go 1.20+) |
| 一次包裝多個錯誤 | fmt.Errorf("... %w ... %w", err1, err2)(Go 1.20+) |
主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6719