Skip to content

Error 处理模式

Go 的错误处理是其最具争议的设计之一,但也是最务实的。掌握正确的错误处理模式,是写出高质量 Go 代码的关键。

基础:error 接口

go
// error 是内置接口
type error interface {
    Error() string
}

// 最简单的错误创建
err1 := errors.New("something went wrong")
err2 := fmt.Errorf("user %d not found", userID)

errors 包核心 API(Go 1.13+)

go
import "errors"

// errors.New — 创建简单错误
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

// fmt.Errorf + %w — 包装错误(保留原始错误)
func getUser(id int) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        return nil, fmt.Errorf("getUser(%d): %w", id, err)
    }
    return user, nil
}

// errors.Is — 检查错误链中是否包含目标错误
err := getUser(42)
if errors.Is(err, ErrNotFound) {
    // 处理未找到的情况
}

// errors.As — 提取错误链中的具体类型
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
    fmt.Println("数据库错误码:", dbErr.Code)
}

// errors.Unwrap — 获取被包装的错误
wrapped := fmt.Errorf("outer: %w", ErrNotFound)
inner := errors.Unwrap(wrapped)  // ErrNotFound

自定义错误类型

go
// 携带上下文信息的错误
type AppError struct {
    Code    int
    Message string
    Err     error  // 原始错误
}

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)
}

// 实现 Unwrap 支持 errors.Is/As 链式查找
func (e *AppError) Unwrap() error {
    return e.Err
}

// 错误码常量
const (
    ErrCodeNotFound   = 404
    ErrCodeForbidden  = 403
    ErrCodeInternal   = 500
)

func NewNotFoundError(resource string, id any) *AppError {
    return &AppError{
        Code:    ErrCodeNotFound,
        Message: fmt.Sprintf("%s(%v) not found", resource, id),
    }
}

错误包装最佳实践

go
// 在调用链中添加上下文,但不要重复
// 好的做法
func (s *UserService) GetProfile(userID int) (*Profile, error) {
    user, err := s.repo.FindUser(userID)
    if err != nil {
        return nil, fmt.Errorf("UserService.GetProfile: %w", err)
    }

    profile, err := s.repo.FindProfile(user.ProfileID)
    if err != nil {
        return nil, fmt.Errorf("UserService.GetProfile: %w", err)
    }

    return profile, nil
}

// 错误链示例:
// UserService.GetProfile: Repository.FindUser: sql: no rows in result set
// 清晰地展示了调用路径

Sentinel 错误(哨兵错误)

go
// 包级别的预定义错误,用于比较
var (
    ErrUserNotFound    = errors.New("user not found")
    ErrInvalidPassword = errors.New("invalid password")
    ErrAccountLocked   = errors.New("account locked")
)

func Login(username, password string) (*User, error) {
    user, err := findUser(username)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrUserNotFound
        }
        return nil, fmt.Errorf("login: %w", err)
    }

    if !checkPassword(user, password) {
        return nil, ErrInvalidPassword
    }

    return user, nil
}

// 调用方
user, err := Login("alice", "wrong")
switch {
case errors.Is(err, ErrUserNotFound):
    http.Error(w, "用户不存在", 404)
case errors.Is(err, ErrInvalidPassword):
    http.Error(w, "密码错误", 401)
case err != nil:
    http.Error(w, "服务器错误", 500)
}

panic 与 recover

go
// panic 用于不可恢复的错误(程序 bug,不是业务错误)
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")  // 程序 bug
    }
    return a / b
}

// recover 只在 defer 中有效
func safeDiv(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered panic: %v", r)
        }
    }()
    return divide(a, b), nil
}

// Web 框架中间件:防止 panic 导致服务崩溃
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // 记录堆栈
                buf := make([]byte, 4096)
                n := runtime.Stack(buf, false)
                log.Printf("panic: %v\n%s", err, buf[:n])
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

多错误处理(Go 1.20+)

go
// errors.Join — 合并多个错误
func validateUser(u User) error {
    var errs []error

    if u.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if u.Age < 0 || u.Age > 150 {
        errs = append(errs, fmt.Errorf("invalid age: %d", u.Age))
    }
    if !isValidEmail(u.Email) {
        errs = append(errs, fmt.Errorf("invalid email: %s", u.Email))
    }

    return errors.Join(errs...)  // nil if errs is empty
}

err := validateUser(User{Age: -1, Email: "bad"})
// err.Error() = "name is required\ninvalid age: -1\ninvalid email: bad"

实战:HTTP 错误处理

go
// 统一的 HTTP 错误响应
type HTTPError struct {
    StatusCode int    `json:"-"`
    Code       string `json:"code"`
    Message    string `json:"message"`
}

func (e *HTTPError) Error() string {
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

var (
    ErrHTTPNotFound   = &HTTPError{404, "NOT_FOUND", "资源不存在"}
    ErrHTTPForbidden  = &HTTPError{403, "FORBIDDEN", "无权访问"}
    ErrHTTPBadRequest = &HTTPError{400, "BAD_REQUEST", "请求参数错误"}
)

// 错误处理中间件
func errorHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 使用 panic 传递错误(某些框架的做法)
        defer func() {
            if err := recover(); err != nil {
                var httpErr *HTTPError
                switch e := err.(type) {
                case *HTTPError:
                    httpErr = e
                case error:
                    httpErr = &HTTPError{500, "INTERNAL_ERROR", e.Error()}
                default:
                    httpErr = &HTTPError{500, "INTERNAL_ERROR", "未知错误"}
                }
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(httpErr.StatusCode)
                json.NewEncoder(w).Encode(httpErr)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

错误处理的 Go 哲学

go
// 1. 错误是值,不是异常
// 显式处理每个可能的错误

// 2. 不要忽略错误
f, err := os.Open("file.txt")
if err != nil {
    return err  // 不要 _ = err
}

// 3. 只包装一次,不要重复包装
// 坏:
return fmt.Errorf("error: %w", fmt.Errorf("error: %w", err))
// 好:
return fmt.Errorf("processFile: %w", err)

// 4. 在包边界转换错误类型
// 内部实现细节不应该泄漏到公共 API

// 5. 使用 log/slog 记录错误上下文
slog.Error("处理请求失败",
    "error", err,
    "user_id", userID,
    "path", r.URL.Path,
)

推荐工具库

  • github.com/pkg/errors — 带堆栈的错误包装(历史库,现在标准库已够用)
  • github.com/cockroachdb/errors — 更强大的错误处理
  • go.uber.org/multierr — 多错误合并(Go 1.20 前的替代方案)

本站内容由 褚成志 整理编写,仅供学习参考