Gin 参数校验:从基础到自定义规则

在前几篇文章中,我们学习了如何处理请求参数、数据解析以及响应渲染。参数校验是确保服务安全性和数据完整性的重要环节。在这篇文章中,我们将聚焦于 Gin 框架中的参数校验 ,并使用强大的 go-playground/validator 库处理请求参数的验证。我们将从基础校验标签到自定义校验规则,带你一步步掌握参数校验的精髓。


1. 使用 go-playground/validator

Gin 框架内置支持 go-playground/validator,这是一个高性能的参数校验库。它支持多种校验规则、数据绑定以及灵活的自定义扩展。


2. 内置校验标签

validator 提供了丰富的校验标签,可以快速验证参数的格式和范围。以下是一些常见标签的用法:

标签 功能
required 必填字段
email 检查是否是有效的电子邮件地址
min / max 数值或字符串的最小/最大值
len 检查字符串的固定长度
gte / lte 检查数值是否大于等于或小于等于某个值

示例:内置校验规则

go 复制代码
type RegisterRequest struct {
	Username string `json:"username" binding:"required,min=3,max=20"`
	Email    string `json:"email" binding:"required,email"`
	Age      int    `json:"age" binding:"gte=1,lte=100"`
	Password string `json:"password" binding:"required,min=6"`
}

r.POST("/register", func(c *gin.Context) {
	var req RegisterRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(400, gin.H{"error": err.Error()})
		return
	}
	c.JSON(200, gin.H{"message": "registration successful"})
})

请求:

json 复制代码
{
  "username": "user",
  "email": "invalid_email",
  "age": 150,
  "password": "123"
}

响应:

json 复制代码
{
  "error": "Key: 'RegisterRequest.Email' Error:Field validation for 'Email' failed on the 'email' tag; Key: 'RegisterRequest.Age' Error:Field validation for 'Age' failed on the 'lte' tag; Key: 'RegisterRequest.Password' Error:Field validation for 'Password' failed on the 'min' tag"
}

这里可以简单说下,为什么会报上面的错,因为在gin中类似于Bind 的方法,主要执行两个逻辑,一个是解析参数,另一个就是参数校验了,比如上面的示例中的ShouldBindJSON,在请求参数传递中,会执行下面方法,先解析出请求参数,再去判断参数是否合理。

go 复制代码
func decodeJSON(r io.Reader, obj any) error {
	decoder := json.NewDecoder(r)
	if EnableDecoderUseNumber {
		decoder.UseNumber()
	}
	if EnableDecoderDisallowUnknownFields {
		decoder.DisallowUnknownFields()
	}
	if err := decoder.Decode(obj); err != nil {
		return err
	}
	return validate(obj)
}

参数校验的核心函数如下

go 复制代码
func (v *Validate) StructCtx(ctx context.Context, s interface{}) (err error) {

	val := reflect.ValueOf(s)
	top := val

	if val.Kind() == reflect.Ptr && !val.IsNil() {
		val = val.Elem()
	}

	if val.Kind() != reflect.Struct || val.Type().ConvertibleTo(timeType) {
		return &InvalidValidationError{Type: reflect.TypeOf(s)}
	}

	// good to validate
	vd := v.pool.Get().(*validate)
	vd.top = top
	vd.isPartial = false
	// vd.hasExcludes = false // only need to reset in StructPartial and StructExcept

	vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)

	if len(vd.errs) > 0 {
		err = vd.errs
		vd.errs = nil
	}

	v.pool.Put(vd)

	return
}

重点来了,那为什么这些tag标签值会去被校验呢,如果随便写一个tag会不会起作用呢?问继续debug ShouldBindJSON 函数,可以看到,在第一次请求的时候,会执行func(v *defaultValidator)lazyinit()方法,而这个函数它调用了初始化validator的逻辑,在这个validator.New()方法中就可以找到我们提出问题的答案。仔细看它的源码,可以看到它在for循环中加载了 bakedInValidators 这个内置校验标签,而这个标签就是我们的结构体校验能够成功的原因了。

go 复制代码
func New(options ...Option) *Validate {

    //此处省略一部分初始化代码
	...

	v := &Validate{
		tagName:     defaultTagName,
		aliases:     make(map[string]string, len(bakedInAliases)),
		validations: make(map[string]internalValidationFuncWrapper, len(bakedInValidators)),
		tagCache:    tc,
		structCache: sc,
	}

    //此处省略一部分代码
	...
	// must copy validators for separate validations to be used in each instance
	for k, val := range bakedInValidators {

		switch k {
		// these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour
		case requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag,
			excludedIfTag, excludedUnlessTag, excludedWithTag, excludedWithAllTag, excludedWithoutTag, excludedWithoutAllTag,
			skipUnlessTag:
			_ = v.registerValidation(k, wrapFunc(val), true, true)
		default:
			// no need to error check here, baked in will always be valid
			_ = v.registerValidation(k, wrapFunc(val), true, false)
		}
	}

	//此处省略一部分代码
	...
	return v
}

相信看完上面对于ShouldBindJSON的debug解析,大家已经知道参数校验是怎么回事了,那就继续往下看如何自定义校验规则呢。


3. 自定义校验函数

除了内置标签,validator 允许开发者编写自定义校验规则。例如,我们可以创建一个校验函数来验证密码强度。

示例:校验密码强度

假设密码必须包含至少一个数字、一个字母和一个特殊字符:

定义自定义校验函数

go 复制代码
import (
	"regexp"

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

func PasswordStrength(fl validator.FieldLevel) bool {
	password := fl.Field().String()
	// 验证至少包含一个字母、一个数字、一个特殊字符
	match, _ := regexp.MatchString(`^(?=.*[a-zA-Z])(?=.*\d)(?=.*[\W_]).+$`, password)
	return match
}

注册自定义规则

go 复制代码
func main() {
	r := gin.Default()

	// 获取验证器实例
	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
		v.RegisterValidation("password_strength", PasswordStrength)
	}

	type RegisterRequest struct {
		Username string `json:"username" binding:"required"`
		Password string `json:"password" binding:"required,password_strength"`
	}

	r.POST("/register", func(c *gin.Context) {
		var req RegisterRequest
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(400, gin.H{"error": err.Error()})
			return
		}
		c.JSON(200, gin.H{"message": "registration successful"})
	})

	r.Run()
}

请求:

json 复制代码
{
  "username": "user",
  "password": "weak"
}

响应:

json 复制代码
{
  "error": "Key: 'RegisterRequest.Password' Error:Field validation for 'Password' failed on the 'password_strength' tag"
}

4. 错误消息国际化与友好提示

在上面的几个示例中,validator 返回的错误消息是英文且偏技术化的。但通常我们的服务是要面向终端的用户时,所以为了更友好的展示错误,可以通过以下方式自定义错误消息。

解析错误并返回友好提示

go 复制代码
func parseValidationError(err error) map[string]string {
	errors := make(map[string]string)
	if validationErrs, ok := err.(validator.ValidationErrors); ok {
		for _, fieldErr := range validationErrs {
			field := fieldErr.Field()
			tag := fieldErr.Tag()
			switch tag {
			case "required":
				errors[field] = "This field is required"
			case "email":
				errors[field] = "Invalid email format"
			case "password_strength":
				errors[field] = "Password must contain a letter, a number, and a special character"
			default:
				errors[field] = "Validation failed"
			}
		}
	}
	return errors
}

更新路由处理函数

go 复制代码
r.POST("/register", func(c *gin.Context) {
	var req RegisterRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		errors := parseValidationError(err)
		c.JSON(400, gin.H{"errors": errors})
		return
	}
	c.JSON(200, gin.H{"message": "registration successful"})
})

请求:

json 复制代码
{
  "username": "",
  "email": "invalid_email",
  "password": "weak"
}

响应:

json 复制代码
{
  "errors": {
    "Username": "This field is required",
    "Email": "Invalid email format",
    "Password": "Password must contain a letter, a number, and a special character"
  }
}

国际化支持

在多语言环境中,可以为不同语言提供定制化的错误提示。例如,可以通过配置映射表动态加载语言资源。

示例:多语言错误消息

go 复制代码
var errorMessages = map[string]map[string]string{
	"en": {
		"required": "This field is required",
		"email":    "Invalid email format",
	},
	"zh": {
		"required": "该字段为必填项",
		"email":    "无效的电子邮件格式",
	},
}

func getErrorMessage(tag string, lang string) string {
	if messages, ok := errorMessages[lang]; ok {
		if msg, exists := messages[tag]; exists {
			return msg
		}
	}
	return "Validation failed"
}

5. 最佳实践

  1. 分离校验逻辑

    • 将自定义规则和通用的错误解析逻辑封装到单独的模块,保持路由处理函数简洁。
  2. 重用校验规则

    • 自定义规则(如密码强度)应该尽量通用,避免重复定义。
  3. 友好的错误提示

    • 面向终端用户的接口应返回直观易懂的错误消息,而非技术性术语。
  4. 多语言支持

    • 对于国际化的应用,提供多语言支持以满足不同地区用户的需求。

ok,以上就是我对gin参数校验输出的全部内容了,通过这篇文章,你已经可以掌握在 Gin 框架中实现参数校验的全流程。从基础的内置校验标签,到自定义规则的开发,再到友好的错误消息提示与国际化支持,都可以帮助你构建更加可靠和易用的服务。在下一篇文章中,我们将探讨如何做全局异常处理,敬请期待! 🚀

相关推荐
小华同学ai28 分钟前
千万别错过!这个国产开源项目彻底改变了你的域名资产管理方式,收藏它相当于多一个安全专家!
前端·后端·github
Vowwwwwww32 分钟前
GIT历史存在大文件的解决办法
前端·git·后端
捡田螺的小男孩44 分钟前
京东一面:接口性能优化,有哪些经验和手段
java·后端·面试
艾露z1 小时前
深度解析Mysql中MVCC的工作机制
java·数据库·后端·mysql
前端付豪1 小时前
揭秘网易统一日志采集与故障定位平台揭秘:如何在亿级请求中1分钟定位线上异常
前端·后端·架构
陈随易1 小时前
Lodash 杀手来了!es-toolkit v1.39.0 已完全兼容4年未更新的 Lodash
前端·后端·程序员
未来影子2 小时前
SpringAI(GA):Nacos3下的分布式MCP
后端·架构·ai编程
Hockor2 小时前
写给前端的 Python 教程三(字符串驻留和小整数池)
前端·后端·python
码农之王2 小时前
记录一次,利用AI DeepSeek,解决工作中算法和无限级树模型问题
后端·算法
Wo3Shi4七2 小时前
消息不丢失:生产者收到写入成功响应后消息一定不会丢失吗?
后端·kafka·消息队列