强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

12 - 错误处理:error 接口、errors 包、自定义错误、panic/recover

12 - 错误处理

12.1 error 接口

// error 是一个内置接口
type error interface {
    Error() string
}
package main

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

func main() {
    // 错误处理的基本模式
    f, err := os.Open("nonexistent.txt")
    if err != nil {
        fmt.Println("打开文件失败:", err)
        return
    }
    defer f.Close()
}

12.2 创建错误

package main

import (
    "errors"
    "fmt"
)

func main() {
    // errors.New
    err1 := errors.New("something went wrong")
    fmt.Println(err1)

    // fmt.Errorf(格式化错误信息)
    name := "Alice"
    err2 := fmt.Errorf("user %s not found", name)
    fmt.Println(err2)

    // Go 1.13+ %w 包装错误
    baseErr := errors.New("connection refused")
    wrappedErr := fmt.Errorf("failed to connect to database: %w", baseErr)
    fmt.Println(wrappedErr)

    // 多个错误包装
    err3 := fmt.Errorf("outer: %w", fmt.Errorf("middle: %w", errors.New("inner")))
    fmt.Println(err3)
}

12.3 检查错误

package main

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

var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

func findUser(id int) (string, error) {
    if id < 0 {
        return "", ErrNotFound
    }
    if id == 0 {
        return "", ErrPermission
    }
    return "Alice", nil
}

func main() {
    // 比较错误
    _, err := findUser(-1)
    if err == ErrNotFound {
        fmt.Println("用户不存在")
    }

    // errors.Is(推荐,支持包装的错误)
    wrappedErr := fmt.Errorf("query failed: %w", ErrNotFound)
    if errors.Is(wrappedErr, ErrNotFound) {
        fmt.Println("用户不存在(通过 Is 检查)")
    }

    // errors.As(提取特定类型的错误)
    _, err = os.Open("nonexistent.txt")
    var pathErr *os.PathError
    if errors.As(err, &pathErr) {
        fmt.Printf("路径错误: %s - %s\n", pathErr.Path, pathErr.Err)
    }

    // 检查多个错误
    _, err = findUser(0)
    switch {
    case errors.Is(err, ErrNotFound):
        fmt.Println("未找到")
    case errors.Is(err, ErrPermission):
        fmt.Println("权限不足")
    default:
        fmt.Println("其他错误:", err)
    }
}

errors.Is vs errors.As

函数用途行为
errors.Is(err, target)检查错误链中是否有特定错误值比较值(支持 Unwrap)
errors.As(err, &target)从错误链中提取特定类型类型断言(支持 Unwrap)

12.4 自定义错误类型

package main

import (
    "fmt"
    "time"
)

// 自定义错误类型
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: field %s - %s", e.Field, e.Message)
}

// 带上下文的错误
type AppError struct {
    Code     int
    Message  string
    Err      error
    CreatedAt time.Time
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// 构造函数
func NewValidationError(field, message string) *ValidationError {
    return &ValidationError{Field: field, Message: message}
}

func NewAppError(code int, message string, err error) *AppError {
    return &AppError{
        Code:      code,
        Message:   message,
        Err:       err,
        CreatedAt: time.Now(),
    }
}

func validateAge(age int) error {
    if age < 0 {
        return NewValidationError("age", "不能为负数")
    }
    if age > 150 {
        return NewValidationError("age", "不能超过150")
    }
    return nil
}

func getUser(id int) (string, error) {
    if id < 0 {
        return "", NewAppError(404, "user not found", ErrNotFound)
    }
    return "Alice", nil
}

func main() {
    err := validateAge(-1)
    if err != nil {
        var ve *ValidationError
        if errors.As(err, &ve) {
            fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
        }
    }
}

12.5 错误处理模式

经典模式

func doSomething() error {
    result, err := step1()
    if err != nil {
        return fmt.Errorf("step1 failed: %w", err)
    }
    
    result2, err := step2(result)
    if err != nil {
        return fmt.Errorf("step2 failed: %w", err)
    }
    
    return step3(result2)
}

哨兵错误

var (
    ErrUserNotFound    = errors.New("user not found")
    ErrUserExists      = errors.New("user already exists")
    ErrInvalidEmail    = errors.New("invalid email")
    ErrDatabaseTimeout = errors.New("database timeout")
)

错误包装链

ErrDatabaseTimeout
  → "failed to query user: database timeout"
    → "API handler error: failed to query user: database timeout"

12.6 panic 和 recover

package main

import "fmt"

func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return a / b, nil
}

func main() {
    // panic 会终止程序
    // divide(10, 0) // panic: division by zero

    // recover 捕获 panic
    result, err := safeDivide(10, 0)
    if err != nil {
        fmt.Println("错误:", err)
    } else {
        fmt.Println("结果:", result)
    }
}

panic 使用原则

✅ 使用 panic 的场景:
   - 程序初始化时无法继续(如配置加载失败)
   - 不可能发生的情况(逻辑上不可达的代码)
   - 内部不可恢复的错误

❌ 不使用 panic 的场景:
   - 可预期的错误(文件不存在、网络超时)
   - 用户输入验证
   - 库函数中(应返回 error)

12.7 错误处理最佳实践

package main

import (
    "errors"
    "fmt"
    "log"
)

// ✅ 及时处理错误
func good() error {
    f, err := os.Open("file.txt")
    if err != nil {
        return err // 立即返回
    }
    defer f.Close()
    // ...
    return nil
}

// ✅ 错误信息应该添加上下文
func wrapError() error {
    err := doOperation()
    if err != nil {
        return fmt.Errorf("failed to process user %d: %w", userID, err)
    }
    return nil
}

// ✅ 使用 errors.Is 比较,不要用字符串比较
func checkErr(err error) bool {
    return errors.Is(err, ErrNotFound) // ✅
    // return err.Error() == "not found" // ❌
}

// ✅ 在边界处处理错误(如 main、HTTP handler)
func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    // 所有错误传播到此处统一处理
    cfg, err := loadConfig()
    if err != nil {
        return fmt.Errorf("loading config: %w", err)
    }
    db, err := connectDB(cfg)
    if err != nil {
        return fmt.Errorf("connecting to DB: %w", err)
    }
    return startServer(db)
}

12.8 Go 1.20+ errors.Join

// 合并多个错误
func validate(name string, age int) error {
    var errs []error
    if name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if age < 0 {
        errs = append(errs, errors.New("age must be positive"))
    }
    return errors.Join(errs...)
}

func main() {
    err := validate("", -1)
    if err != nil {
        fmt.Println(err)
        // name is required
        // age must be positive

        // errors.Is 可以检查任一错误
        fmt.Println(errors.Is(err, errors.New("name is required"))) // false(新创建的错误)
    }
}

🏢 业务场景

  1. API 错误码:自定义错误类型包含 HTTP 状态码
  2. 数据库操作:包装 SQL 错误为业务错误
  3. 配置验证:累积多个验证错误一次性返回
  4. 中间件恢复:defer+recover 捕获 panic,防止服务崩溃
  5. 重试机制:判断错误类型决定是否可重试

📖 扩展阅读