文章

错误处理

错误处理

error 接口

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

// 最简单的自定义错误
type MyError struct {
    Code    int
    Message string
    Cause   error  // 原始错误
}

func (e *MyError) Error() string {
    if e.Cause != nil {
        return fmt.Sprintf("code=%d msg=%s cause=%s", e.Code, e.Message, e.Cause)
    }
    return fmt.Sprintf("code=%d msg=%s", e.Code, e.Message)
}

func (e *MyError) Unwrap() error {
    return e.Cause
}

错误创建方式对比

方式代码性能信息量推荐场景
errors.Newerrors.New("not found")最快静态错误消息
fmt.Errorffmt.Errorf("user %s not found", id)格式化错误消息
fmt.Errorf + %wfmt.Errorf("get user: %w", err)错误包装
自定义 struct&MyError{Code: 404, ...}最高需要结构化错误信息
errors.Joinerrors.Join(err1, err2)合并多个错误(Go 1.20+)

错误包装与解包(Go 1.13+)

// 包装错误
func getUser(id string) (*User, error) {
    row := db.QueryRow("SELECT ... WHERE id = ?", id)
    var u User
    if err := row.Scan(&u.Name, &u.Email); err != nil {
        return nil, fmt.Errorf("scan user %s: %w", id, err)  // %w 包装
    }
    return &u, nil
}

// 调用方
user, err := getUser("123")
if err != nil {
    // 日志记录完整错误链
    log.Printf("failed: %+v", err)

    // 判断是否是特定错误
    var sqlErr *mysql.MySQLError
    if errors.Is(err, sql.ErrNoRows) {
        // 用户不存在
    } else if errors.As(err, &sqlErr) {
        // MySQL 特定错误
        log.Printf("mysql error code: %d", sqlErr.Number)
    }
    return
}

errors.Is vs errors.As

函数比较方式用途示例
errors.Is(err, target)值比较(沿 Unwrap 链)判断错误是否是特定值errors.Is(err, sql.ErrNoRows)
errors.As(err, &target)类型比较(沿 Unwrap 链)提取特定类型的错误errors.As(err, &mysqlErr)
// errors.Is:检查错误值
if errors.Is(err, context.DeadlineExceeded) {
    // 超时错误
}

// errors.As:提取错误类型
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
    fmt.Println("path:", pathErr.Path)
}

// errors.Join:合并多个错误(Go 1.20+)
err := errors.Join(
    errors.New("validation failed"),
    errors.New("name is required"),
    errors.New("email is invalid"),
)
// 输出:validation failed\nname is required\nemail is invalid

错误处理最佳实践

1. 哨兵错误(Sentinel Errors)

// 预定义错误变量
var (
    ErrUserNotFound  = errors.New("user not found")
    ErrInvalidInput  = errors.New("invalid input")
    ErrUnauthorized  = errors.New("unauthorized")
)

// 使用
func GetUser(id string) (*User, error) {
    user, err := repo.FindByID(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrUserNotFound  // 转换为业务错误
        }
        return nil, fmt.Errorf("find user: %w", err)
    }
    return user, nil
}

// 调用方判断
user, err := GetUser("123")
if errors.Is(err, ErrUserNotFound) {
    http.Error(w, "User not found", http.StatusNotFound)
    return
}

2. 自定义错误类型

// 带业务码的错误
type AppError struct {
    Code    string  // 业务错误码,如 "USER_NOT_FOUND"
    Message string  // 用户友好的消息
    Err     error   // 原始错误
}

func (e *AppError) Error() string {
    return e.Message
}

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

// 构造函数
func NewAppError(code, msg string, err error) *AppError {
    return &AppError{Code: code, Message: msg, Err: err}
}

// 使用
func GetUser(id string) (*User, error) {
    user, err := repo.FindByID(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, NewAppError("USER_NOT_FOUND", "用户不存在", err)
        }
        return nil, NewAppError("DB_ERROR", "数据库异常", err)
    }
    return user, nil
}

// 调用方
user, err := GetUser("123")
if err != nil {
    var appErr *AppError
    if errors.As(err, &appErr) {
        switch appErr.Code {
        case "USER_NOT_FOUND":
            http.Error(w, appErr.Message, http.StatusNotFound)
        case "DB_ERROR":
            http.Error(w, appErr.Message, http.StatusInternalServerError)
        }
        return
    }
    // 未知错误
    http.Error(w, "internal error", http.StatusInternalServerError)
}

3. 错误处理分层

Repository 层:返回数据库原始错误
    ↓ fmt.Errorf 包装
Service 层:转换为业务错误 + 上下文
    ↓ errors.As 判断
Handler 层:转换为 HTTP 状态码 + 用户友好消息
// Repository 层
func (r *UserRepo) FindByID(ctx context.Context, id string) (*entity.User, error) {
    var u entity.User
    err := r.db.WithContext(ctx).First(&u, "id = ?", id).Error
    if err != nil {
        return nil, fmt.Errorf("query user %s: %w", id, err)
    }
    return &u, nil
}

// Service 层
func (s *UserService) GetByID(ctx context.Context, id string) (*dto.UserDTO, error) {
    entity, err := s.repo.FindByID(ctx, id)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, ErrUserNotFound
        }
        return nil, fmt.Errorf("get user %s: %w", id, err)
    }
    return dto.FromEntity(entity), nil
}

// Handler 层
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    user, err := h.svc.GetByID(r.Context(), id)
    if err != nil {
        switch {
        case errors.Is(err, ErrUserNotFound):
            response.JSON(w, http.StatusNotFound, map[string]string{
                "error": "用户不存在",
            })
        default:
            log.Printf("get user failed: %v", err)
            response.JSON(w, http.StatusInternalServerError, map[string]string{
                "error": "内部错误",
            })
        }
        return
    }
    response.JSON(w, http.StatusOK, user)
}

panic 与 recover

// panic:程序遇到不可恢复的错误
func divide(a, b int) int {
    if b == 0 {
        panic("division by zero")
    }
    return a / b
}

// recover:捕获 panic,防止程序崩溃
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("recovered: %v", r)
        }
    }()
    return divide(a, b), nil
}

何时用 panic / 何时用 error

场景用 error用 panic
可预期的业务错误
用户输入错误
网络超时
文件不存在
数组越界✓(编程错误)
nil 指针解引用✓(编程错误)
初始化失败✓(程序无法继续运行)
不可能到达的分支✓(default 分支防御)

panic 在 HTTP 中的恢复

// Recovery 中间件
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic recovered: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

错误处理检查清单

检查项说明
✅ 不要忽略错误_ = doSomething() 要有明确理由
✅ 错误要包装上下文fmt.Errorf("doing X: %w", err)
✅ 哨兵错误用 errors.Is 判断不要用 == 或字符串匹配
✅ 自定义错误用 errors.As 提取沿 Unwrap 链查找
✅ error 只返回一次在合适的层级返回,不要层层传递原始错误
✅ 日志记录和错误返回不要重复记录 OR 返回,不要既记录又返回
✅ 面向用户的错误消息要友好”用户不存在”而非 “record not found”
✅ 不要用 panic 做正常的错误处理panic 只用于不可恢复的错误