错误处理最佳实践

良好的错误处理习惯能让代码更健壮,问题排查更高效。

统一错误定义

集中定义所有错误:

package errors

import "errors"

var (
    ErrUserNotFound     = errors.New("用户不存在")
    ErrUserDisabled     = errors.New("用户已被禁用")
    ErrPasswordWrong    = errors.New("密码错误")
    ErrTokenExpired     = errors.New("登录已过期")
    ErrTokenInvalid     = errors.New("无效的登录凭证")
    ErrPermissionDenied = errors.New("权限不足")
)

type BizError struct {
    Code    int
    Message string
    Details interface{}
}

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

func NewBizError(code int, message string, details ...interface{}) *BizError {
    e := &BizError{
        Code:    code,
        Message: message,
    }
    if len(details) > 0 {
        e.Details = details[0]
    }
    return e
}

错误码规范

const (
    CodeSuccess = 0
    
    CodeBadRequest      = 400
    CodeUnauthorized    = 401
    CodeForbidden       = 403
    CodeNotFound        = 404
    CodeConflict        = 409
    CodeInternalError   = 500
    
    CodeUserNotFound    = 10001
    CodeUserDisabled    = 10002
    CodePasswordWrong   = 10003
    CodeTokenExpired    = 10004
    
    CodeOrderNotFound   = 20001
    CodeOrderPaid       = 20002
    CodeOrderCancelled  = 20003
)

var codeMessages = map[int]string{
    CodeSuccess:         "成功",
    CodeBadRequest:      "请求参数错误",
    CodeUnauthorized:    "未授权",
    CodeForbidden:       "禁止访问",
    CodeNotFound:        "资源不存在",
    CodeUserNotFound:    "用户不存在",
    CodeUserDisabled:    "用户已被禁用",
    CodePasswordWrong:   "密码错误",
    CodeTokenExpired:    "登录已过期",
    CodeOrderNotFound:   "订单不存在",
    CodeOrderPaid:       "订单已支付",
    CodeOrderCancelled:  "订单已取消",
}

func GetMessage(code int) string {
    if msg, ok := codeMessages[code]; ok {
        return msg
    }
    return "未知错误"
}

统一响应格式

type Response struct {
    Code    int         `json:"code"`
    Message string      `json:"message"`
    Data    interface{} `json:"data,omitempty"`
    Errors  []ErrorItem `json:"errors,omitempty"`
}

type ErrorItem struct {
    Field   string `json:"field,omitempty"`
    Message string `json:"message"`
}

func Success(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, Response{
        Code:    CodeSuccess,
        Message: GetMessage(CodeSuccess),
        Data:    data,
    })
}

func Fail(c *gin.Context, code int, errors ...ErrorItem) {
    c.JSON(http.StatusOK, Response{
        Code:    code,
        Message: GetMessage(code),
        Errors:  errors,
    })
}

r.GET("/user/:id", func(c *gin.Context) {
    user, err := getUser(c.Param("id"))
    if err != nil {
        Fail(c, CodeUserNotFound)
        return
    }
    
    Success(c, user)
})

错误包装

import "errors"

func WrapError(err error, message string) error {
    if err == nil {
        return nil
    }
    return fmt.Errorf("%s: %w", message, err)
}

func GetUser(id string) (*User, error) {
    user, err := db.FindUser(id)
    if err != nil {
        return nil, WrapError(err, "查询用户失败")
    }
    return user, nil
}

r.GET("/user/:id", func(c *gin.Context) {
    user, err := GetUser(c.Param("id"))
    if err != nil {
        log.Printf("获取用户失败: %v", err)
        Fail(c, CodeUserNotFound)
        return
    }
    Success(c, user)
})

错误日志规范

func LogError(c *gin.Context, err error, fields ...interface{}) {
    requestID, _ := c.Get("requestID")
    
    entry := log.NewEntry(log.StandardLogger())
    entry = entry.WithField("request_id", requestID)
    entry = entry.WithField("path", c.Request.URL.Path)
    entry = entry.WithField("method", c.Request.Method)
    
    for i := 0; i < len(fields); i += 2 {
        if i+1 < len(fields) {
            entry = entry.WithField(fields[i].(string), fields[i+1])
        }
    }
    
    entry.Error(err.Error())
}

r.GET("/test", func(c *gin.Context) {
    user, err := getUser("123")
    if err != nil {
        LogError(c, err, "user_id", "123", "action", "get_user")
        Fail(c, CodeUserNotFound)
        return
    }
    Success(c, user)
})

错误恢复中间件

func RecoveryMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                requestID, _ := c.Get("requestID")
                
                log.Printf("[PANIC] request_id=%s, error=%v\n%s",
                    requestID, err, debug.Stack())
                
                c.AbortWithStatusJSON(http.StatusInternalServerError, Response{
                    Code:    CodeInternalError,
                    Message: "服务器内部错误",
                })
            }
        }()
        c.Next()
    }
}

错误处理中间件

func ErrorHandlerMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            var errors []ErrorItem
            for _, e := range c.Errors.ByType(gin.ErrorTypePublic) {
                errors = append(errors, ErrorItem{
                    Message: e.Error(),
                })
            }
            
            if len(errors) > 0 {
                c.JSON(http.StatusBadRequest, Response{
                    Code:    CodeBadRequest,
                    Message: "请求处理失败",
                    Errors:  errors,
                })
            }
        }
    }
}

验证错误处理

func HandleValidationError(c *gin.Context, err error) {
    var errors []ErrorItem
    
    if validationErrors, ok := err.(validator.ValidationErrors); ok {
        for _, e := range validationErrors {
            errors = append(errors, ErrorItem{
                Field:   e.Field(),
                Message: getValidationMessage(e),
            })
        }
    } else {
        errors = append(errors, ErrorItem{
            Message: err.Error(),
        })
    }
    
    Fail(c, CodeBadRequest, errors...)
}

func getValidationMessage(e validator.FieldError) string {
    switch e.Tag() {
    case "required":
        return e.Field() + "为必填项"
    case "email":
        return "邮箱格式不正确"
    case "min":
        return e.Field() + "最小长度为" + e.Param()
    case "max":
        return e.Field() + "最大长度为" + e.Param()
    default:
        return e.Field() + "验证失败"
    }
}

r.POST("/user", func(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        HandleValidationError(c, err)
        return
    }
    
    Success(c, nil)
})

小结

良好的错误处理需要:统一错误定义、规范错误码、统一响应格式、合理记录日志。错误信息要对用户友好,同时保留足够的调试信息。使用中间件统一处理错误,避免代码重复。