• gin binding方法汇总


    为了能够更方便的获取请求相关参数,提高开发效率,gin框架提供了Bind系列方法。

    绑定方法分类

    Bind系列方法大致分两类:

    1. Should类型
    2. Must类型

    二者的区别就是:should和must的区别,即must绑定失败会报错

    绑定方法关系图

    Should类型

    在这里插入图片描述

    Must类型

    在这里插入图片描述

    ShouldBindBodyWith

    在这里插入图片描述
    假设有这样的场景:

    1. 请求体中的数据分为:校验用数据和接口用数据
    2. 校验用数据绑定给auth中间件的结构体,api用数据绑定为api结构体
    3. 一个请求上下分则需要绑定两次,但request.Body只可读取一次

    此时,shouldBindBodyWith就应用而生:

    1. ShouldBindBodyWith is similar with ShouldBindWith, but it stores the request
      body into the context, and reuse when it is called again.
    2. 将request.body再次存储在上下文中
    3. 后续调用都得使用该方法

    校验器

    1. gin框架使用github.com/go-playground/validator进行参数校验,目前已经支持github.com/go-playground/validator/v10了。
    2. 文档:validator
    3. github:validator

    简单示例

    package main
    
    import (
    	"net/http"
    
    	"github.com/gin-gonic/gin"
    )
    
    type SignUpParam struct {
    	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    	Name       string `json:"name" binding:"required"`
    	Email      string `json:"email" binding:"required,email"`
    	Password   string `json:"password" binding:"required"`
    	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    }
    
    func main() {
    	r := gin.Default()
    
    	r.POST("/signup", func(c *gin.Context) {
    		var u SignUpParam
    		if err := c.ShouldBind(&u); err != nil {
    			c.JSON(http.StatusOK, gin.H{
    				"msg": err.Error(),
    			})
    			return
    		}
    		// 保存入库等业务逻辑代码...
    
    		c.JSON(http.StatusOK, "success")
    	})
    
    	_ = r.Run(":8999")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    此时,若gin的Bind方法会根据binding标签校验字段,发送一个请求,查看报错信息:

    {"msg":"Key: 'SignUpParam.Email' Error:Field validation for 'Email' failed on the 'email' tag\nKey: 'SignUpParam.Password' Error:Field validation for 'Password' failed on the 'required' tag\nKey: 'SignUpParam.RePassword' Error:Field validation for 'RePassword' failed on the 'required' tag"}
    
    • 1

    从最终的输出结果可以看到 validator 的检验生效了,但是错误提示的字段不是特别友好,我们可能需要将它翻译成中文。

    翻译校验错误提示信息

    validator库本身是支持国际化的,借助相应的语言包可以实现校验错误提示信息的自动翻译。

    package main
    
    import (
    	"fmt"
    	"net/http"
    
    	"github.com/gin-gonic/gin"
    	"github.com/gin-gonic/gin/binding"
    	"github.com/go-playground/locales/en"
    	"github.com/go-playground/locales/zh"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	enTranslations "github.com/go-playground/validator/v10/translations/en"
    	zhTranslations "github.com/go-playground/validator/v10/translations/zh"
    )
    
    // 定义一个全局翻译器T
    var trans ut.Translator
    
    // InitTrans 初始化翻译器
    func InitTrans(locale string) (err error) {
    	// 修改gin框架中的Validator引擎属性,实现自定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
    		zhT := zh.New() // 中文翻译器
    		enT := en.New() // 英文翻译器
    
    		// 第一个参数是备用(fallback)的语言环境
    		// 后面的参数是应该支持的语言环境(支持多个)
    		// uni := ut.New(zhT, zhT) 也是可以的
    		uni := ut.New(enT, zhT, enT)
    
    		// locale 通常取决于 http 请求头的 'Accept-Language'
    		var ok bool
    		// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
    		trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
    		}
    
    		// 注册翻译器
    		switch locale {
    		case "en":
    			err = enTranslations.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			err = zhTranslations.RegisterDefaultTranslations(v, trans)
    		default:
    			err = enTranslations.RegisterDefaultTranslations(v, trans)
    		}
    		return
    	}
    	return
    }
    
    type SignUpParam struct {
    	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    	Name       string `json:"name" binding:"required"`
    	Email      string `json:"email" binding:"required,email"`
    	Password   string `json:"password" binding:"required"`
    	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    }
    
    func main() {
    	if err := InitTrans("zh"); err != nil {
    		fmt.Printf("init trans failed, err:%v\n", err)
    		return
    	}
    
    	r := gin.Default()
    
    	r.POST("/signup", func(c *gin.Context) {
    		var u SignUpParam
    		if err := c.ShouldBind(&u); err != nil {
    			// 获取validator.ValidationErrors类型的errors
    			errs, ok := err.(validator.ValidationErrors)
    			if !ok {
    				// 非validator.ValidationErrors类型错误直接返回
    				c.JSON(http.StatusOK, gin.H{
    					"msg": err.Error(),
    				})
    				return
    			}
    			// validator.ValidationErrors类型错误则进行翻译
    			c.JSON(http.StatusOK, gin.H{
    				"msg":errs.Translate(trans),
    			})
    			return
    		}
    		// 保存入库等具体业务逻辑代码...
    
    		c.JSON(http.StatusOK, "success")
    	})
    
    	_ = r.Run(":8999")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    同样的请求再来一次,查看报错信息:

    {"msg":{"SignUpParam.Email":"Email必须是一个有效的邮箱","SignUpParam.Password":"Password为必填字段","SignUpParam.RePassword":"RePassword为必填字段"}}

    自定义错误提示信息的字段名

    上面的错误提示看起来是可以了,但是还是差点意思,首先是错误提示中的字段并不是请求中使用的字段,例如:RePassword是我们后端定义的结构体中的字段名,而请求中使用的是re_password字段。

    只需要在初始化翻译器的时候像下面一样添加一个获取json tag的自定义方法即可。

    // InitTrans 初始化翻译器
    func InitTrans(locale string) (err error) {
    	// 修改gin框架中的Validator引擎属性,实现自定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
    		// 注册一个获取json tag的自定义方法
    		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
    			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    			if name == "-" {
    				return ""
    			}
    			return name
    		})
    
    		zhT := zh.New() // 中文翻译器
    		enT := en.New() // 英文翻译器
    
    		// 第一个参数是备用(fallback)的语言环境
    		// 后面的参数是应该支持的语言环境(支持多个)
    		// uni := ut.New(zhT, zhT) 也是可以的
    		uni := ut.New(enT, zhT, enT)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    再次发送请求,查看错误信息:
    {"msg":{"SignUpParam.email":"email必须是一个有效的邮箱","SignUpParam.password":"password为必填字段","SignUpParam.re_password":"re_password为必填字段"}}

    但是还是有点瑕疵,那就是最终的错误提示信息中心还是有我们后端定义的结构体名称——SignUpParam,这个名称其实是不需要随错误提示返回给前端的,前端并不需要这个值。

    定义一个去掉结构体名称前缀的自定义方法:

    func removeTopStruct(fields map[string]string) map[string]string {
    	res := map[string]string{}
    	for field, err := range fields {
    		res[field[strings.Index(field, ".")+1:]] = err
    	}
    	return res
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在代码中使用上述函数将翻译后的errors做一下处理即可:

    if err := c.ShouldBind(&u); err != nil {
    	// 获取validator.ValidationErrors类型的errors
    	errs, ok := err.(validator.ValidationErrors)
    	if !ok {
    		// 非validator.ValidationErrors类型错误直接返回
    		c.JSON(http.StatusOK, gin.H{
    			"msg": err.Error(),
    		})
    		return
    	}
    	// validator.ValidationErrors类型错误则进行翻译
    	// 并使用removeTopStruct函数去除字段名中的结构体名称标识
    	c.JSON(http.StatusOK, gin.H{
    		"msg": removeTopStruct(errs.Translate(trans)),
    	})
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    最终报错信息:
    {"msg":{"email":"email必须是一个有效的邮箱","password":"password为必填字段","re_password":"re_password为必填字段"}}

    自定义结构体校验方法

    上面的校验还是有点小问题,就是当涉及到一些复杂的校验规则,比如re_password字段需要与password字段的值相等这样的校验规则,我们的自定义错误提示字段名称方法就不能很好解决错误提示信息中的其他字段名称了。

    报错信息:
    {"msg":{"email":"email必须是一个有效的邮箱","re_password":"re_password必须等于Password"}}

    此时如果想要追求更好的提示效果,将上面的Password字段也改为和json tag一致的名称,就需要我们自定义结构体校验的方法。

    为SignUpParam自定义一个校验方法如下:

    type SignUpParam struct {
    	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    	Name       string `json:"name" binding:"required"`
    	Email      string `json:"email" binding:"required,email"`
    	Password   string `json:"password" binding:"required"`
    	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    // SignUpParamStructLevelValidation 自定义SignUpParam结构体校验函数
    func SignUpParamStructLevelValidation(sl validator.StructLevel) {
    	su := sl.Current().Interface().(SignUpParam)
    
    	if su.Password != su.RePassword {
    		// 输出错误提示信息,最后一个参数就是传递的param
    		sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    然后在初始化校验器的函数中注册该自定义校验方法即可:

    func InitTrans(locale string) (err error) {
    	// 修改gin框架中的Validator引擎属性,实现自定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
    		// ... liwenzhou.com ...
        
    		// 为SignUpParam注册自定义校验方法
    		v.RegisterStructValidation(SignUpParamStructLevelValidation, SignUpParam{})
    
    		zhT := zh.New() // 中文翻译器
    		enT := en.New() // 英文翻译器
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    报错信息:
    {"msg":{"email":"email必须是一个有效的邮箱","re_password":"re_password必须等于password"}}

    自定义字段校验方法

    除了上面介绍到的自定义结构体校验方法,validator还支持为某个字段自定义校验方法,并使用RegisterValidation()注册到校验器实例中。

    为SignUpParam添加一个需要使用自定义校验方法checkDate做参数校验的字段Date。

    type SignUpParam struct {
    	Age        uint8  `json:"age" binding:"gte=1,lte=130"`
    	Name       string `json:"name" binding:"required"`
    	Email      string `json:"email" binding:"required,email"`
    	Password   string `json:"password" binding:"required"`
    	RePassword string `json:"re_password" binding:"required,eqfield=Password"`
    	// 需要使用自定义校验方法checkDate做参数校验的字段Date
    	Date       string `json:"date" binding:"required,datetime=2006-01-02,checkDate"`
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中datetime=2006-01-02是内置的用于校验日期类参数是否满足指定格式要求的tag。 如果传入的date参数不满足2006-01-02这种格式就会提示如下错误:{"msg":{"date":"date的格式必须是2006-01-02"}}

    首先我们要在需要执行自定义校验的字段后面添加自定义tag,这里使用的是checkDate:

    // customFunc 自定义字段级别校验方法
    func customFunc(fl validator.FieldLevel) bool {
    	date, err := time.Parse("2006-01-02", fl.Field().String())
    	if err != nil {
    		return false
    	}
    	if date.Before(time.Now()) {
    		return false
    	}
    	return true
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    定义好了字段及其自定义校验方法后,就需要将它们联系起来并注册到校验器实例中:

    // 在校验器注册自定义的校验方法
    if err := v.RegisterValidation("checkDate", customFunc); err != nil {
    	return err
    }
    
    • 1
    • 2
    • 3
    • 4

    此时的报错信息:
    {"msg":{"date":"Key: 'SignUpParam.date' Error:Field validation for 'date' failed on the 'checkDate' tag"}}

    自定义翻译方法

    现在需要为自定义字段校验方法提供一个自定义的翻译方法,从而实现该字段错误提示信息的自定义显示。

    // registerTranslator 为自定义字段添加翻译功能
    func registerTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
    	return func(trans ut.Translator) error {
    		if err := trans.Add(tag, msg, false); err != nil {
    			return err
    		}
    		return nil
    	}
    }
    
    // translate 自定义字段的翻译方法
    func translate(trans ut.Translator, fe validator.FieldError) string {
    	msg, err := trans.T(fe.Tag(), fe.Field())
    	if err != nil {
    		panic(fe.(error).Error())
    	}
    	return msg
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    定义好了相关翻译方法之后,在InitTrans函数中通过调用RegisterTranslation()方法来注册我们自定义的翻译方法。

    // InitTrans 初始化翻译器
    func InitTrans(locale string) (err error) {
    	// ...liwenzhou.com...
    	
    		// 注册翻译器
    		switch locale {
    		case "en":
    			err = enTranslations.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			err = zhTranslations.RegisterDefaultTranslations(v, trans)
    		default:
    			err = enTranslations.RegisterDefaultTranslations(v, trans)
    		}
    		if err != nil {
    			return err
    		}
    		// 注意!因为这里会使用到trans实例
    		// 所以这一步注册要放到trans初始化的后面
    		if err := v.RegisterTranslation(
    			"checkDate",
    			trans,
    			registerTranslator("checkDate", "{0}必须要晚于当前日期"),
    			translate,
    		); err != nil {
    			return err
    		}
    		return
    	}
    	return
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    报错信息:
    {"msg":{"date":"date必须要晚于当前日期"}}

    完整自定义校验器代码

    package validators
    
    import (
    	"errors"
    	"fmt"
    	"github.com/gin-gonic/gin/binding"
    	"github.com/go-playground/locales/en"
    	"github.com/go-playground/locales/zh"
    	ut "github.com/go-playground/universal-translator"
    	"github.com/go-playground/validator/v10"
    	enTranslations "github.com/go-playground/validator/v10/translations/en"
    	zhTranslations "github.com/go-playground/validator/v10/translations/zh"
    	"reflect"
    	"strings"
    	"time"
    )
    
    const (
    	DateValidatorKetWord = "checkDate"
    )
    
    // InitTrans 初始化翻译器
    // 具体参考:https://blog.csdn.net/General_zy/article/details/127938889
    func InitTrans(locale string) (ut.Translator, error) {
    	// 修改gin框架中的Validator引擎属性,实现自定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    
    		// 注册一个获取json tag的自定义方法
    		v.RegisterTagNameFunc(func(fld reflect.StructField) string {
    			name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
    			if name == "-" {
    				return ""
    			}
    			return name
    		})
    
    		zhT := zh.New() // 中文翻译器
    		enT := en.New() // 英文翻译器
    
    		// 第一个参数是备用(fallback)的语言环境
    		// 后面的参数是应该支持的语言环境(支持多个)
    		// uni := ut.New(zhT, zhT) 也是可以的
    		uni := ut.New(enT, zhT, enT)
    
    		// locale 通常取决于 http 请求头的 'Accept-Language'
    		// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
    		trans, ok := uni.GetTranslator(locale)
    		if !ok {
    			return nil, fmt.Errorf("uni.GetTranslator(%s) failed", locale)
    		}
    
    		var err error
    		// 注册翻译器
    		switch locale {
    		case "en":
    			err = enTranslations.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			err = zhTranslations.RegisterDefaultTranslations(v, trans)
    		default:
    			err = enTranslations.RegisterDefaultTranslations(v, trans)
    		}
    
    		// 在校验器注册自定义的校验方法
    		if err = v.RegisterValidation(DateValidatorKetWord, DateValidator); err != nil {
    			return nil, err
    		}
    		// 注册自定义的校验字段
    		if err = v.RegisterTranslation(
    			DateValidatorKetWord,
    			trans,
    			RegisterTranslator(DateValidatorKetWord, "{0}必须要晚于当前日期"),
    			Translate,
    		); err != nil {
    			return nil, err
    		}
    
    		return trans, err
    	}
    	return nil, errors.New("failed to initialize translator")
    }
    
    // DateValidator 日期校验器,校验传入time.Date是否晚于当前时间
    func DateValidator(fl validator.FieldLevel) bool {
    	date, err := time.Parse("2006-01-02", fl.Field().String())
    	if err != nil {
    		return false
    	}
    	if date.Before(time.Now()) {
    		return false
    	}
    	return true
    }
    
    // RegisterTranslator 为自定义字段添加翻译功能
    func RegisterTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
    	return func(trans ut.Translator) error {
    		if err := trans.Add(tag, msg, false); err != nil {
    			return err
    		}
    		return nil
    	}
    }
    
    // Translate 自定义字段的翻译方法
    func Translate(trans ut.Translator, fe validator.FieldError) string {
    	msg, err := trans.T(fe.Tag(), fe.Field())
    	if err != nil {
    		panic(fe.(error).Error())
    	}
    	return msg
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    注意事项

    required认为值为0不通过校验

    gin框架中一般用通过定义结构体中的字段Tag标签来设置参数校验规则和解析字段名,转json相关信息,其中字段是否为必须项规则写在字段Tag:binding:"required"

    解决方案:

    把整数字段改成整数指针类型,指针没有默认值0,也就没有这个问题了,注意取值加*,设值加&总之记得他是指针就好了

    小结

    gin框架中validator的使用技巧同样也适用于直接使用validator库,区别仅仅在于配置的是gin框架中的校验器还是由validator.New()创建的校验器。

  • 相关阅读:
    【更新!】3dMax材质ID随机生成器插件MaterialIDsRandomGenerator v2.1.2使用教程
    《CTF攻防世界web题》之茶壶我爱你(2)
    TensorFlow简要介绍
    PHP代码审计12—反序列化漏洞
    「引流工具」火炬多平台多功能引流高效推广脚本,抖音+快手+小红书多平台自动引流软件
    编程技术对网络社会的益作用?
    【FI】财务预制凭证界面隐藏过账按钮
    maven环境变量,安装源,本地仓库配置
    GO结构体
    python-继承
  • 原文地址:https://blog.csdn.net/General_zy/article/details/127938889