验证
Fox 通过结构体标签和自定义验证器提供灵活的验证选项。
Fox 使用 go-playground/validator 库进行验证。
type UserInput struct { // 必填字段 Name string `json:"name" binding:"required"`
// 邮箱验证 Email string `json:"email" binding:"required,email"`
// 字符串长度 Username string `json:"username" binding:"required,min=3,max=20"`
// 数值范围 Age int `json:"age" binding:"required,gte=0,lte=130"`
// URL 验证 Website string `json:"website" binding:"omitempty,url"`
// 枚举验证 Role string `json:"role" binding:"required,oneof=admin user guest"`}IsValider 接口
Section titled “IsValider 接口”实现 IsValider 接口以自定义验证逻辑:
type SignupRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` Confirm string `json:"confirm" binding:"required"`}
func (r *SignupRequest) IsValid() error { if r.Password != r.Confirm { return errors.New("密码不匹配") }
if len(r.Password) < 8 { return errors.New("密码必须至少 8 个字符") }
// 检查密码复杂度 if !hasUpperCase(r.Password) || !hasLowerCase(r.Password) || !hasDigit(r.Password) { return errors.New("密码必须包含大写字母、小写字母和数字") }
return nil}Fox 在绑定和内置验证之后自动调用 IsValid()。
type CreateOrderRequest struct { UserID int `json:"user_id" binding:"required"` Items []Item `json:"items" binding:"required,min=1,dive"` Address Address `json:"address" binding:"required"`}
func (r *CreateOrderRequest) IsValid() error { // 检查用户是否存在 if !userExists(r.UserID) { return errors.New("用户未找到") }
// 验证总金额 total := 0.0 for _, item := range r.Items { if item.Quantity <= 0 { return fmt.Errorf("商品 %d 的数量无效", item.ID) } total += item.Price * float64(item.Quantity) }
if total <= 0 { return errors.New("订单总额必须大于 0") }
return nil}默认错误响应
Section titled “默认错误响应”当验证失败时,Fox 返回 400 Bad Request:
{ "error": "Key: 'CreateUserRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag"}自定义错误处理器
Section titled “自定义错误处理器”自定义错误响应:
r := fox.New()
r.SetValidationErrorHandler(func(c *gin.Context, err error) { if validationErr, ok := err.(validator.ValidationErrors); ok { errors := make(map[string]string)
for _, e := range validationErr { field := e.Field() tag := e.Tag()
// 自定义错误消息 switch tag { case "required": errors[field] = fmt.Sprintf("%s 为必填项", field) case "email": errors[field] = "邮箱格式无效" case "min": errors[field] = fmt.Sprintf("%s 必须至少 %s 个字符", field, e.Param()) case "max": errors[field] = fmt.Sprintf("%s 最多 %s 个字符", field, e.Param()) default: errors[field] = fmt.Sprintf("'%s' 验证失败", tag) } }
c.JSON(400, gin.H{"errors": errors}) return }
c.JSON(400, gin.H{"error": err.Error()})})响应:
{ "errors": { "Email": "邮箱格式无效", "Password": "Password 必须至少 8 个字符" }}自定义验证器
Section titled “自定义验证器”注册自定义验证函数:
import "github.com/go-playground/validator/v10"
func init() { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("username", validateUsername) }}
func validateUsername(fl validator.FieldLevel) bool { username := fl.Field().String()
// 自定义逻辑:仅字母数字和下划线 matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username) return matched}
type UserInput struct { Username string `json:"username" binding:"required,username"`}验证字段之间的关系:
func init() { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("gtefield_date", gtefieldDate) }}
type EventInput struct { StartDate time.Time `json:"start_date" binding:"required"` EndDate time.Time `json:"end_date" binding:"required,gtefield_date=StartDate"`}
func gtefieldDate(fl validator.FieldLevel) bool { endDate := fl.Field() startDateField := fl.Parent().FieldByName(fl.Param())
if !startDateField.IsValid() { return false }
return endDate.Interface().(time.Time).After(startDateField.Interface().(time.Time))}Required If
Section titled “Required If”type ContactForm struct { PreferEmail bool `json:"prefer_email"` Email string `json:"email" binding:"required_if=PreferEmail true,omitempty,email"` Phone string `json:"phone" binding:"required_unless=PreferEmail true"`}type PaymentRequest struct { Method string `json:"method" binding:"required,oneof=credit_card paypal"` CardNumber string `json:"card_number" binding:"required_if=Method credit_card"` PayPalEmail string `json:"paypal_email" binding:"required_if=Method paypal,omitempty,email"`}func TestUserValidation(t *testing.T) { tests := []struct { name string input SignupRequest wantErr bool }{ { name: "有效输入", input: SignupRequest{ Username: "john_doe", Password: "SecurePass123", Confirm: "SecurePass123", }, wantErr: false, }, { name: "密码不匹配", input: SignupRequest{ Username: "john_doe", Password: "SecurePass123", Confirm: "DifferentPass", }, wantErr: true, }, { name: "弱密码", input: SignupRequest{ Username: "john_doe", Password: "weak", Confirm: "weak", }, wantErr: true, }, }
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.input.IsValid() if (err != nil) != tt.wantErr { t.Errorf("IsValid() error = %v, wantErr %v", err, tt.wantErr) } }) }}-
结合结构体标签和自定义验证
type Input struct {Email string `json:"email" binding:"required,email"` // 结构体标签}func (i *Input) IsValid() error {// 自定义业务逻辑if isBlacklisted(i.Email) {return errors.New("邮箱在黑名单中")}return nil} -
提供清晰的错误消息
- 使用字段名,而不是结构体标签
- 解释错误所在以及如何修复
- 支持国际化
-
早期验证 - 对无效输入快速失败
-
不要重复数据库约束 - 但要验证业务规则
-
测试验证逻辑 - 为 IsValid() 方法编写单元测试