3.3.3 Gin 错误处理与验证

3.3.3 Gin 错误处理与验证 #

在 Web 应用开发中,错误处理和数据验证是确保应用健壮性和安全性的关键环节。本节将深入探讨 Gin 框架的错误处理机制、数据验证功能以及如何构建统一的错误响应系统。

统一错误处理 #

错误类型定义 #

首先定义应用中常见的错误类型和结构:

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "net/http"
)

// 错误代码常量
const (
    ErrCodeValidation    = "VALIDATION_ERROR"
    ErrCodeNotFound      = "NOT_FOUND"
    ErrCodeUnauthorized  = "UNAUTHORIZED"
    ErrCodeForbidden     = "FORBIDDEN"
    ErrCodeInternal      = "INTERNAL_ERROR"
    ErrCodeBadRequest    = "BAD_REQUEST"
    ErrCodeConflict      = "CONFLICT"
    ErrCodeTooManyReq    = "TOO_MANY_REQUESTS"
)

// 应用错误结构
type AppError struct {
    Code       string      `json:"code"`
    Message    string      `json:"message"`
    Details    interface{} `json:"details,omitempty"`
    StatusCode int         `json:"-"`
}

func (e *AppError) Error() string {
    return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}

// 错误构造函数
func NewValidationError(message string, details interface{}) *AppError {
    return &AppError{
        Code:       ErrCodeValidation,
        Message:    message,
        Details:    details,
        StatusCode: http.StatusBadRequest,
    }
}

func NewNotFoundError(message string) *AppError {
    return &AppError{
        Code:       ErrCodeNotFound,
        Message:    message,
        StatusCode: http.StatusNotFound,
    }
}

func NewUnauthorizedError(message string) *AppError {
    return &AppError{
        Code:       ErrCodeUnauthorized,
        Message:    message,
        StatusCode: http.StatusUnauthorized,
    }
}

func NewForbiddenError(message string) *AppError {
    return &AppError{
        Code:       ErrCodeForbidden,
        Message:    message,
        StatusCode: http.StatusForbidden,
    }
}

func NewInternalError(message string) *AppError {
    return &AppError{
        Code:       ErrCodeInternal,
        Message:    message,
        StatusCode: http.StatusInternalServerError,
    }
}

func NewConflictError(message string) *AppError {
    return &AppError{
        Code:       ErrCodeConflict,
        Message:    message,
        StatusCode: http.StatusConflict,
    }
}

全局错误处理中间件 #

import (
    "log"
    "runtime/debug"
)

// 全局错误处理中间件
func errorHandlingMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if err := recover(); err != nil {
                // 记录 panic 信息
                log.Printf("Panic recovered: %v\n%s", err, debug.Stack())

                // 返回内部服务器错误
                appErr := NewInternalError("Internal server error")
                c.JSON(appErr.StatusCode, appErr)
                c.Abort()
            }
        }()

        c.Next()

        // 处理在处理函数中添加的错误
        if len(c.Errors) > 0 {
            err := c.Errors.Last()

            switch e := err.Err.(type) {
            case *AppError:
                c.JSON(e.StatusCode, e)
            default:
                // 未知错误,返回内部服务器错误
                appErr := NewInternalError("Internal server error")
                c.JSON(appErr.StatusCode, appErr)
            }

            c.Abort()
        }
    }
}

// 错误处理辅助函数
func HandleError(c *gin.Context, err error) {
    if appErr, ok := err.(*AppError); ok {
        c.Error(appErr)
    } else {
        c.Error(NewInternalError(err.Error()))
    }
}

// 成功响应辅助函数
func SuccessResponse(c *gin.Context, data interface{}) {
    c.JSON(http.StatusOK, gin.H{
        "success": true,
        "data":    data,
    })
}

func CreatedResponse(c *gin.Context, data interface{}) {
    c.JSON(http.StatusCreated, gin.H{
        "success": true,
        "data":    data,
    })
}

业务逻辑错误处理 #

// 用户服务示例
type UserService struct {
    // 数据库连接等
}

func (s *UserService) GetUser(id uint) (*User, error) {
    // 模拟数据库查询
    if id == 0 {
        return nil, NewValidationError("Invalid user ID", map[string]string{
            "id": "User ID must be greater than 0",
        })
    }

    // 模拟用户不存在
    if id == 999 {
        return nil, NewNotFoundError("User not found")
    }

    // 模拟数据库错误
    if id == 500 {
        return nil, fmt.Errorf("database connection failed")
    }

    return &User{ID: id, Name: "Test User"}, nil
}

func (s *UserService) CreateUser(req CreateUserRequest) (*User, error) {
    // 检查用户名是否已存在
    if s.usernameExists(req.Username) {
        return nil, NewConflictError("Username already exists")
    }

    // 检查邮箱是否已存在
    if s.emailExists(req.Email) {
        return nil, NewConflictError("Email already exists")
    }

    // 创建用户
    user := &User{
        Username: req.Username,
        Email:    req.Email,
        // ... 其他字段
    }

    return user, nil
}

func (s *UserService) usernameExists(username string) bool {
    // 模拟检查逻辑
    return username == "admin"
}

func (s *UserService) emailExists(email string) bool {
    // 模拟检查逻辑
    return email == "[email protected]"
}

// 控制器中的错误处理
func getUserHandler(c *gin.Context) {
    var uri struct {
        ID uint `uri:"id" binding:"required"`
    }

    if err := c.ShouldBindUri(&uri); err != nil {
        HandleError(c, NewValidationError("Invalid URI parameters", parseValidationErrors(err)))
        return
    }

    userService := &UserService{}
    user, err := userService.GetUser(uri.ID)
    if err != nil {
        HandleError(c, err)
        return
    }

    SuccessResponse(c, user)
}

func createUserHandler(c *gin.Context) {
    var req CreateUserRequest

    if err := c.ShouldBindJSON(&req); err != nil {
        HandleError(c, NewValidationError("Invalid request data", parseValidationErrors(err)))
        return
    }

    userService := &UserService{}
    user, err := userService.CreateUser(req)
    if err != nil {
        HandleError(c, err)
        return
    }

    CreatedResponse(c, user)
}

数据验证机制 #

基础验证规则 #

Gin 使用 validator 包进行数据验证,支持丰富的验证规则:

import (
    "github.com/go-playground/validator/v10"
)

// 用户注册请求结构
type RegisterRequest struct {
    Username        string `json:"username" binding:"required,min=3,max=20,alphanum"`
    Email          string `json:"email" binding:"required,email"`
    Password       string `json:"password" binding:"required,min=8,max=50"`
    ConfirmPassword string `json:"confirm_password" binding:"required,eqfield=Password"`
    Age            int    `json:"age" binding:"required,min=18,max=120"`
    Phone          string `json:"phone" binding:"omitempty,e164"`
    Website        string `json:"website" binding:"omitempty,url"`
    Gender         string `json:"gender" binding:"omitempty,oneof=male female other"`
    Terms          bool   `json:"terms" binding:"required,eq=true"`
}

// 用户更新请求结构
type UpdateUserRequest struct {
    Username string `json:"username" binding:"omitempty,min=3,max=20,alphanum"`
    Email    string `json:"email" binding:"omitempty,email"`
    Age      int    `json:"age" binding:"omitempty,min=18,max=120"`
    Phone    string `json:"phone" binding:"omitempty,e164"`
    Website  string `json:"website" binding:"omitempty,url"`
    Bio      string `json:"bio" binding:"omitempty,max=500"`
}

// 查询参数验证
type UserListQuery struct {
    Page     int    `form:"page,default=1" binding:"min=1"`
    Size     int    `form:"size,default=10" binding:"min=1,max=100"`
    Sort     string `form:"sort,default=created_at" binding:"omitempty,oneof=id username email created_at updated_at"`
    Order    string `form:"order,default=desc" binding:"omitempty,oneof=asc desc"`
    Status   string `form:"status" binding:"omitempty,oneof=active inactive banned"`
    Gender   string `form:"gender" binding:"omitempty,oneof=male female other"`
    MinAge   int    `form:"min_age" binding:"omitempty,min=0,max=120"`
    MaxAge   int    `form:"max_age" binding:"omitempty,min=0,max=120,gtefield=MinAge"`
    Keyword  string `form:"keyword" binding:"omitempty,min=2,max=50"`
}

// 批量操作请求
type BatchRequest struct {
    Action  string `json:"action" binding:"required,oneof=activate deactivate delete ban unban"`
    UserIDs []uint `json:"user_ids" binding:"required,min=1,max=100,dive,min=1"`
    Reason  string `json:"reason" binding:"required_if=Action ban,omitempty,max=200"`
}

自定义验证规则 #

import (
    "regexp"
    "strings"
    "unicode"
)

// 注册自定义验证器
func registerCustomValidators() {
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        // 用户名验证:字母开头,可包含字母、数字、下划线
        v.RegisterValidation("username", func(fl validator.FieldLevel) bool {
            username := fl.Field().String()
            if len(username) < 3 || len(username) > 20 {
                return false
            }

            // 必须以字母开头
            if !unicode.IsLetter(rune(username[0])) {
                return false
            }

            // 只能包含字母、数字、下划线
            for _, char := range username {
                if !unicode.IsLetter(char) && !unicode.IsDigit(char) && char != '_' {
                    return false
                }
            }

            return true
        })

        // 强密码验证
        v.RegisterValidation("strong_password", func(fl validator.FieldLevel) bool {
            password := fl.Field().String()

            if len(password) < 8 {
                return false
            }

            var (
                hasUpper   = false
                hasLower   = false
                hasNumber  = false
                hasSpecial = false
            )

            for _, char := range password {
                switch {
                case unicode.IsUpper(char):
                    hasUpper = true
                case unicode.IsLower(char):
                    hasLower = true
                case unicode.IsDigit(char):
                    hasNumber = true
                case unicode.IsPunct(char) || unicode.IsSymbol(char):
                    hasSpecial = true
                }
            }

            return hasUpper && hasLower && hasNumber && hasSpecial
        })

        // 中国手机号验证
        v.RegisterValidation("china_mobile", func(fl validator.FieldLevel) bool {
            phone := fl.Field().String()
            matched, _ := regexp.MatchString(`^1[3-9]\d{9}$`, phone)
            return matched
        })

        // 身份证号验证(简化版)
        v.RegisterValidation("id_card", func(fl validator.FieldLevel) bool {
            idCard := fl.Field().String()
            if len(idCard) != 18 {
                return false
            }

            matched, _ := regexp.MatchString(`^\d{17}[\dXx]$`, idCard)
            return matched
        })

        // 标签验证
        v.RegisterValidation("tags", func(fl validator.FieldLevel) bool {
            tags := fl.Field().Interface().([]string)

            if len(tags) > 10 {
                return false
            }

            tagMap := make(map[string]bool)
            for _, tag := range tags {
                // 检查标签长度
                if len(tag) < 2 || len(tag) > 20 {
                    return false
                }

                // 检查标签格式
                if !regexp.MustCompile(`^[a-zA-Z0-9\u4e00-\u9fa5_-]+$`).MatchString(tag) {
                    return false
                }

                // 检查重复标签
                if tagMap[tag] {
                    return false
                }
                tagMap[tag] = true
            }

            return true
        })

        // 跨字段验证:确认密码
        v.RegisterValidation("confirm_password", func(fl validator.FieldLevel) bool {
            confirmPassword := fl.Field().String()
            password := fl.Parent().FieldByName("Password").String()
            return confirmPassword == password
        })
    }
}

// 使用自定义验证器的结构体
type AdvancedRegisterRequest struct {
    Username        string   `json:"username" binding:"required,username"`
    Email          string   `json:"email" binding:"required,email"`
    Password       string   `json:"password" binding:"required,strong_password"`
    ConfirmPassword string   `json:"confirm_password" binding:"required,confirm_password"`
    Phone          string   `json:"phone" binding:"required,china_mobile"`
    IDCard         string   `json:"id_card" binding:"omitempty,id_card"`
    Tags           []string `json:"tags" binding:"omitempty,tags"`
    Age            int      `json:"age" binding:"required,min=18,max=120"`
    Terms          bool     `json:"terms" binding:"required,eq=true"`
}

验证错误解析 #

// 验证错误详细信息
type ValidationError struct {
    Field   string `json:"field"`
    Tag     string `json:"tag"`
    Value   string `json:"value"`
    Message string `json:"message"`
}

// 解析验证错误
func parseValidationErrors(err error) []ValidationError {
    var validationErrors []ValidationError

    if validationErrs, ok := err.(validator.ValidationErrors); ok {
        for _, fieldError := range validationErrs {
            validationError := ValidationError{
                Field: fieldError.Field(),
                Tag:   fieldError.Tag(),
                Value: fmt.Sprintf("%v", fieldError.Value()),
            }

            // 根据验证标签生成友好的错误消息
            switch fieldError.Tag() {
            case "required":
                validationError.Message = fmt.Sprintf("%s is required", fieldError.Field())
            case "email":
                validationError.Message = "Invalid email format"
            case "min":
                validationError.Message = fmt.Sprintf("%s must be at least %s characters/value", fieldError.Field(), fieldError.Param())
            case "max":
                validationError.Message = fmt.Sprintf("%s must be at most %s characters/value", fieldError.Field(), fieldError.Param())
            case "len":
                validationError.Message = fmt.Sprintf("%s must be exactly %s characters", fieldError.Field(), fieldError.Param())
            case "alphanum":
                validationError.Message = fmt.Sprintf("%s must contain only alphanumeric characters", fieldError.Field())
            case "oneof":
                validationError.Message = fmt.Sprintf("%s must be one of: %s", fieldError.Field(), fieldError.Param())
            case "eqfield":
                validationError.Message = fmt.Sprintf("%s must match %s", fieldError.Field(), fieldError.Param())
            case "gtefield":
                validationError.Message = fmt.Sprintf("%s must be greater than or equal to %s", fieldError.Field(), fieldError.Param())
            case "ltefield":
                validationError.Message = fmt.Sprintf("%s must be less than or equal to %s", fieldError.Field(), fieldError.Param())
            case "url":
                validationError.Message = "Invalid URL format"
            case "e164":
                validationError.Message = "Invalid phone number format"
            case "username":
                validationError.Message = "Username must start with a letter and contain only letters, numbers, and underscores"
            case "strong_password":
                validationError.Message = "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character"
            case "china_mobile":
                validationError.Message = "Invalid Chinese mobile phone number"
            case "id_card":
                validationError.Message = "Invalid ID card number"
            case "tags":
                validationError.Message = "Invalid tags format or too many tags"
            case "confirm_password":
                validationError.Message = "Passwords do not match"
            case "dive":
                validationError.Message = fmt.Sprintf("Invalid item in %s array", fieldError.Field())
            default:
                validationError.Message = fmt.Sprintf("Invalid %s", fieldError.Field())
            }

            validationErrors = append(validationErrors, validationError)
        }
    }

    return validationErrors
}

// 验证中间件
func validationMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()

        // 检查验证错误
        for _, ginErr := range c.Errors {
            if validationErrs, ok := ginErr.Err.(validator.ValidationErrors); ok {
                errors := parseValidationErrors(validationErrs)
                appErr := NewValidationError("Validation failed", errors)
                c.JSON(appErr.StatusCode, appErr)
                c.Abort()
                return
            }
        }
    }
}

错误响应格式化 #

统一响应格式 #

// 统一响应结构
type Response struct {
    Success   bool        `json:"success"`
    Data      interface{} `json:"data,omitempty"`
    Error     *ErrorInfo  `json:"error,omitempty"`
    Meta      *MetaInfo   `json:"meta,omitempty"`
    Timestamp int64       `json:"timestamp"`
}

type ErrorInfo struct {
    Code    string      `json:"code"`
    Message string      `json:"message"`
    Details interface{} `json:"details,omitempty"`
}

type MetaInfo struct {
    RequestID string `json:"request_id,omitempty"`
    Version   string `json:"version,omitempty"`
    Page      *Page  `json:"page,omitempty"`
}

type Page struct {
    Current int `json:"current"`
    Size    int `json:"size"`
    Total   int `json:"total"`
    Pages   int `json:"pages"`
}

// 响应构建器
type ResponseBuilder struct {
    c *gin.Context
}

func NewResponseBuilder(c *gin.Context) *ResponseBuilder {
    return &ResponseBuilder{c: c}
}

func (rb *ResponseBuilder) Success(data interface{}) {
    response := Response{
        Success:   true,
        Data:      data,
        Timestamp: time.Now().Unix(),
    }

    // 添加请求 ID
    if requestID, exists := rb.c.Get("request_id"); exists {
        response.Meta = &MetaInfo{
            RequestID: requestID.(string),
            Version:   "v1",
        }
    }

    rb.c.JSON(http.StatusOK, response)
}

func (rb *ResponseBuilder) SuccessWithPagination(data interface{}, page Page) {
    response := Response{
        Success:   true,
        Data:      data,
        Timestamp: time.Now().Unix(),
        Meta: &MetaInfo{
            Page: &page,
        },
    }

    if requestID, exists := rb.c.Get("request_id"); exists {
        response.Meta.RequestID = requestID.(string)
        response.Meta.Version = "v1"
    }

    rb.c.JSON(http.StatusOK, response)
}

func (rb *ResponseBuilder) Error(appErr *AppError) {
    response := Response{
        Success: false,
        Error: &ErrorInfo{
            Code:    appErr.Code,
            Message: appErr.Message,
            Details: appErr.Details,
        },
        Timestamp: time.Now().Unix(),
    }

    if requestID, exists := rb.c.Get("request_id"); exists {
        response.Meta = &MetaInfo{
            RequestID: requestID.(string),
            Version:   "v1",
        }
    }

    rb.c.JSON(appErr.StatusCode, response)
}

func (rb *ResponseBuilder) Created(data interface{}) {
    response := Response{
        Success:   true,
        Data:      data,
        Timestamp: time.Now().Unix(),
    }

    if requestID, exists := rb.c.Get("request_id"); exists {
        response.Meta = &MetaInfo{
            RequestID: requestID.(string),
            Version:   "v1",
        }
    }

    rb.c.JSON(http.StatusCreated, response)
}

完整的错误处理示例 #

// 完整的用户管理 API 示例
func setupCompleteErrorHandling() *gin.Engine {
    r := gin.New()

    // 注册自定义验证器
    registerCustomValidators()

    // 应用中间件
    r.Use(requestTrackingMiddleware())
    r.Use(errorHandlingMiddleware())
    r.Use(validationMiddleware())

    // 用户路由
    api := r.Group("/api/v1")
    {
        users := api.Group("/users")
        {
            users.GET("", getUserListWithErrorHandling)
            users.POST("", createUserWithErrorHandling)
            users.GET("/:id", getUserWithErrorHandling)
            users.PUT("/:id", updateUserWithErrorHandling)
            users.DELETE("/:id", deleteUserWithErrorHandling)
        }
    }

    return r
}

func getUserListWithErrorHandling(c *gin.Context) {
    var query UserListQuery

    if err := c.ShouldBindQuery(&query); err != nil {
        rb := NewResponseBuilder(c)
        appErr := NewValidationError("Invalid query parameters", parseValidationErrors(err))
        rb.Error(appErr)
        return
    }

    // 业务逻辑
    users, total, err := getUsersFromDatabase(query)
    if err != nil {
        HandleError(c, err)
        return
    }

    // 构建分页信息
    page := Page{
        Current: query.Page,
        Size:    query.Size,
        Total:   total,
        Pages:   (total + query.Size - 1) / query.Size,
    }

    rb := NewResponseBuilder(c)
    rb.SuccessWithPagination(users, page)
}

func createUserWithErrorHandling(c *gin.Context) {
    var req AdvancedRegisterRequest

    if err := c.ShouldBindJSON(&req); err != nil {
        rb := NewResponseBuilder(c)
        appErr := NewValidationError("Invalid request data", parseValidationErrors(err))
        rb.Error(appErr)
        return
    }

    // 业务逻辑
    user, err := createUserInDatabase(req)
    if err != nil {
        HandleError(c, err)
        return
    }

    rb := NewResponseBuilder(c)
    rb.Created(user)
}

func getUserWithErrorHandling(c *gin.Context) {
    var uri struct {
        ID uint `uri:"id" binding:"required,min=1"`
    }

    if err := c.ShouldBindUri(&uri); err != nil {
        rb := NewResponseBuilder(c)
        appErr := NewValidationError("Invalid user ID", parseValidationErrors(err))
        rb.Error(appErr)
        return
    }

    user, err := getUserFromDatabase(uri.ID)
    if err != nil {
        HandleError(c, err)
        return
    }

    rb := NewResponseBuilder(c)
    rb.Success(user)
}

// 模拟数据库操作
func getUsersFromDatabase(query UserListQuery) ([]User, int, error) {
    // 模拟数据库错误
    if query.Keyword == "error" {
        return nil, 0, fmt.Errorf("database connection failed")
    }

    // 模拟返回数据
    users := []User{
        {ID: 1, Username: "user1", Email: "[email protected]"},
        {ID: 2, Username: "user2", Email: "[email protected]"},
    }

    return users, len(users), nil
}

func createUserInDatabase(req AdvancedRegisterRequest) (*User, error) {
    // 模拟用户名冲突
    if req.Username == "admin" {
        return nil, NewConflictError("Username already exists")
    }

    // 模拟邮箱冲突
    if req.Email == "[email protected]" {
        return nil, NewConflictError("Email already exists")
    }

    user := &User{
        ID:       1,
        Username: req.Username,
        Email:    req.Email,
    }

    return user, nil
}

func getUserFromDatabase(id uint) (*User, error) {
    if id == 999 {
        return nil, NewNotFoundError("User not found")
    }

    return &User{
        ID:       id,
        Username: "testuser",
        Email:    "[email protected]",
    }, nil
}

通过本节的学习,你已经掌握了 Gin 框架中完整的错误处理和数据验证机制。这些技术能够帮助你构建健壮、用户友好的 Web API,确保应用的稳定性和安全性。在下一节中,我们将学习 Gin 的渲染与模板功能。