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 的渲染与模板功能。