错误收集

Gin 的 Context 提供了错误收集功能,可以在处理过程中累积多个错误,最后统一处理。

基本用法

使用 c.Error() 添加错误:

package main

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

func main() {
    r := gin.Default()
    
    r.GET("/error", func(c *gin.Context) {
        c.Error(errors.New("第一个错误"))
        c.Error(errors.New("第二个错误"))
        
        c.JSON(http.StatusOK, gin.H{"message": "处理完成"})
    })
    
    r.Run(":8080")
}

获取收集的错误

使用 c.Errors 获取所有错误:

r.GET("/errors", func(c *gin.Context) {
    c.Error(errors.New("错误A")).SetType(gin.ErrorTypePrivate)
    c.Error(errors.New("错误B")).SetType(gin.ErrorTypePublic)
    c.Error(errors.New("错误C")).SetType(gin.ErrorTypePublic)
    
    var publicErrors []string
    for _, err := range c.Errors {
        if err.Type == gin.ErrorTypePublic {
            publicErrors = append(publicErrors, err.Error())
        }
    }
    
    c.JSON(http.StatusBadRequest, gin.H{
        "errors": publicErrors,
    })
})

错误类型

Gin 定义了三种错误类型:

const (
    ErrorTypePrivate = 1 << iota
    ErrorTypePublic
    ErrorTypeAny = ErrorTypePrivate | ErrorTypePublic
)
  • ErrorTypePrivate - 私有错误,不暴露给用户
  • ErrorTypePublic - 公开错误,可以展示给用户
  • ErrorTypeAny - 所有错误

按类型获取错误

r.GET("/by-type", func(c *gin.Context) {
    c.Error(errors.New("私有错误")).SetType(gin.ErrorTypePrivate)
    c.Error(errors.New("公开错误1")).SetType(gin.ErrorTypePublic)
    c.Error(errors.New("公开错误2")).SetType(gin.ErrorTypePublic)
    
    publicErrors := c.Errors.ByType(gin.ErrorTypePublic)
    
    var messages []string
    for _, err := range publicErrors {
        messages = append(messages, err.Error())
    }
    
    c.JSON(http.StatusBadRequest, gin.H{
        "errors": messages,
    })
})

错误附带元数据

r.POST("/validate", func(c *gin.Context) {
    var user struct {
        Name  string `json:"name" binding:"required"`
        Email string `json:"email" binding:"required,email"`
        Age   int    `json:"age" binding:"required,gte=0,lte=150"`
    }
    
    if err := c.ShouldBindJSON(&user); err != nil {
        c.Error(err).SetType(gin.ErrorTypePublic).SetMeta(gin.H{
            "field": "request_body",
            "code":  "invalid_format",
        })
    }
    
    if user.Name == "admin" {
        c.Error(errors.New("用户名已存在")).SetType(gin.ErrorTypePublic).SetMeta(gin.H{
            "field": "name",
            "code":  "duplicate",
        })
    }
    
    if len(c.Errors) > 0 {
        var errors []gin.H
        for _, e := range c.Errors.ByType(gin.ErrorTypePublic) {
            errors = append(errors, gin.H{
                "message": e.Error(),
                "meta":    e.Meta,
            })
        }
        c.JSON(http.StatusBadRequest, gin.H{"errors": errors})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"message": "验证通过"})
})

统一错误处理中间件

func ErrorHandler() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        if len(c.Errors) > 0 {
            var errors []gin.H
            for _, e := range c.Errors.ByType(gin.ErrorTypePublic) {
                err := gin.H{
                    "message": e.Error(),
                }
                if e.Meta != nil {
                    err["meta"] = e.Meta
                }
                errors = append(errors, err)
            }
            
            if len(errors) > 0 {
                c.JSON(http.StatusBadRequest, gin.H{
                    "code":    400,
                    "message": "请求处理失败",
                    "errors":  errors,
                })
                return
            }
        }
    }
}

r.Use(ErrorHandler())

错误处理的实际应用

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
}

func ValidateUser(c *gin.Context, user *User) bool {
    valid := true
    
    if user.Name == "" {
        c.Error(errors.New("用户名不能为空")).
            SetType(gin.ErrorTypePublic).
            SetMeta(ValidationError{Field: "name", Message: "required"})
        valid = false
    }
    
    if user.Email == "" {
        c.Error(errors.New("邮箱不能为空")).
            SetType(gin.ErrorTypePublic).
            SetMeta(ValidationError{Field: "email", Message: "required"})
        valid = false
    }
    
    if user.Age < 0 || user.Age > 150 {
        c.Error(errors.New("年龄必须在0-150之间")).
            SetType(gin.ErrorTypePublic).
            SetMeta(ValidationError{Field: "age", Message: "range"})
        valid = false
    }
    
    return valid
}

r.POST("/users", func(c *gin.Context) {
    var user User
    c.ShouldBindJSON(&user)
    
    if !ValidateUser(c, &user) {
        return
    }
    
    c.JSON(http.StatusOK, gin.H{"message": "创建成功"})
})

最后一个错误

r.GET("/last-error", func(c *gin.Context) {
    c.Error(errors.New("错误1"))
    c.Error(errors.New("错误2"))
    c.Error(errors.New("错误3"))
    
    lastError := c.Errors.Last()
    c.String(http.StatusOK, "最后一个错误: %s", lastError.Error())
})

清空错误

r.GET("/clear-errors", func(c *gin.Context) {
    c.Error(errors.New("临时错误"))
    
    c.Errors = c.Errors[0:0]
    
    c.JSON(http.StatusOK, gin.H{"message": "错误已清空"})
})

小结

Gin 的错误收集机制提供了一种优雅的方式来累积和处理多个错误。通过 ErrorType 区分公开和私有错误,通过 Meta 附加额外信息。配合中间件可以实现统一的错误响应格式。