gin框架中对前端传递的数据进行校验(包括国际化)

一、为什么要数据校验

  • 数据校验有点类似中间件的作用,使前端传递过来的数据更加合法的到达控制器,以减少数据在控制层中一个一个的校验。
  • 如果不使用数据校验,要不就自己手动的在控制器中一个一个的数据校验
  • go语言中是没有类似java一样的注解的方式来校验数据,但是可以在结构体中使用校验方法来校验(本人不是特别的喜欢这种方式,但是对于这门语言也只能这样)

二、在go语言中使用数据校验

  • 1、官网地址

    https://github.com/go-playground/validator
    
  • 2、安装依赖包

    go get github.com/go-playground/validator
    
  • 3、基础的使用参考官网

三、自定义校验器

  • 1、定义接收参数的结构体

    type UserInfo struct {
    	Id   string `validate:"uuid" json:"id"`           //UUID 类型
    	Name string `validate:"checkName,required" json:"name"`     // 自定义校验
    	Age  uint8  `validate:"min=0,max=130,required" json:"age"` // 年龄
    }
    
  • 2、自定义方法

    func checkNameFunc(f validator.FieldLevel) bool {
    	count := utf8.RuneCountInString(f.Field().String())
    	if count >= 2 && count <= 12 {
    		return true
    	} else {
    		return false
    	}
    }
    
  • 3、在init函数中注册方法

    var valildate *validator.Validate
    
    func init() {
    	valildate = validator.New()
    	valildate.RegisterValidation("checkName", checkNameFunc)
    }
    
  • 4、在main函数中使用

    func main() {
    	router := gin.Default()
    	router.POST("/register", func(c *gin.Context) {
    		user := UserInfo{}
    		//或者var user = UserInfo
    		err := c.Bind(&user)
    		if err != nil {
    			c.JSON(http.StatusOK, gin.H{
    				"code":    1,
    				"message": "请求参数错误",
    			})
    			return
    		}
    		err = valildate.Struct(user)
    		if err != nil {
    			// 输出校验错误 .(validator.ValidationErrors)是断言
    			for _, e := range err.(validator.ValidationErrors) {
    				fmt.Println("错误字段:", e.Field())
    				fmt.Println("错误的值:", e.Value())
    				fmt.Println("错误的tag:", e.Tag())
    			}
    			c.JSON(http.StatusOK, gin.H{
    				"code":    1,
    				"message": "数据校验错误",
    			})
    			return
    		}
    		fmt.Println(user, "接收的数据")
    		c.JSON(http.StatusOK, gin.H{
    			"code":    0,
    			"message": "请求成功",
    		})
    	})
    	router.Run(":8000")
    }
    

四、对于多层嵌套数据的校验

  • 1、对于多层结构体的数据校验使用dive

  • 2、入参的结构体

    type UserInfo struct {
    	Id   string `validate:"uuid" json:"id"`           //UUID 类型
    	Name string `validate:"checkName,required" json:"name"`     // 自定义校验
    	Age  uint8  `validate:"min=0,max=130,required" json:"age"` // 年龄
    	Address []Address `validate:"dive" json:"address"` // 收货地址
    }
    
    type Address struct {
    	Province string `json:"province" validate:"required"` // 省份
    	City string `json:"city" validate:"required"` // 市
    	County string `json:"county" validate:"required"` //县
    	Mobile string `json:"mobile" validate:"numeric,len=11"` // 手机号码
    }
    
  • 3、使用postman提交数据

    {
        "name": "admin",
        "age": 20,
        "id": "3f7a783f-f9dc-4db8-9c6b-afe1d3f9b3f7",
        "address": [
            {
                "province": "广东省",
                "city":"深圳市",
                "county": "宝安区",
                "mobile": "18112345678"
            }
        ]
    }
    
  • 4、别的和上面一样的结构

五、翻译成中文错误提示

  • 1、定义简单的注册结构体

    type RegisterForm struct {
    	UserName   string `json:"username" binding:"required,min=3,max=10"`
    	Password   string `json:"password" binding:"required,min=3"`
    	RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
    }
    
  • 2、常规的使用提示错误

    {
      "error": "Key: 'RegisterForm.RePassword' Error:Field validation for 'RePassword' failed on the 'eqfield' tag"
    }
    
  • 3、使用翻译成中文,可读性更加好,定义一个翻译器的方法

    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"
    	en_translations "github.com/go-playground/validator/v10/translations/en"
    	zh_translations "github.com/go-playground/validator/v10/translations/zh"
    )
    
    var trans ut.Translator
    
    //定义翻译的方法
    func InitTrans(locale string) (err error) {
    	//修改gin框架中的validator引擎属性, 实现定制
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		zhT := zh.New() //中文翻译器
    		enT := en.New() //英文翻译器
    		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
    		uni := ut.New(enT, zhT, enT)
    		trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("uni.GetTranslator(%s)", locale)
    		}
    		switch locale {
    		case "en":
    			en_translations.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			zh_translations.RegisterDefaultTranslations(v, trans)
    		default:
    			en_translations.RegisterDefaultTranslations(v, trans)
    		}
    		return
    	}
    	return
    }
    
  • 4、使用翻译器

    func main() {
    	if err := InitTrans("zh"); err != nil {
    		fmt.Println("初始化翻译器错误")
    		return
    	}
    	router := gin.Default()
    	router.POST("/register", func(c *gin.Context) {
    		var registerForm RegisterForm
    		if err := c.ShouldBind(&registerForm); err != nil {
    			fmt.Println(err.Error())
    			// 翻译错误
    			errs, ok := err.(validator.ValidationErrors)
    			if !ok {
    				c.JSON(http.StatusOK, gin.H{
    					"message": err.Error(),
    				})
    			}
          // 将错误信息返回给前端
    			c.JSON(http.StatusOK, gin.H{
    				"error": errs.Translate(trans),
    			})
    			return
    		}
    		fmt.Println("注册的参数:", registerForm)
    		c.JSON(http.StatusOK, gin.H{
    			"code":    1,
    			"message": "成功",
    			"data": gin.H{
    				"username": registerForm.UserName,
    				"password": registerForm.Password,
    			},
    		})
    	})
    	router.Run(":8080")
    }
    
    {
      "error": {
        "RegisterForm.RePassword": "RePassword必须等于Password",
        "RegisterForm.UserName": "UserName长度必须至少为3个字符"
      }
    }
    
  • 5、移除多余的标签,让提示更加友好

    func removeTopStruct(fileds map[string]string) map[string]string {
    	rsp := map[string]string{}
    	for field, err := range fileds {
    		rsp[field[strings.Index(field, ".")+1:]] = err
    	}
    	return rsp
    }
    
    if err := c.ShouldBind(&registerForm); err != nil {
      fmt.Println(err.Error())
      // 翻译错误
      errs, ok := err.(validator.ValidationErrors)
      if !ok {
        c.JSON(http.StatusOK, gin.H{
          "message": err.Error(),
        })
      }
      c.JSON(http.StatusOK, gin.H{
        "error": removeTopStruct(errs.Translate(trans)),
      })
      return
    }
    
  • 6、关于翻译成中文的完整代码

    package main
    
    import (
    	"fmt"
    	"net/http"
    	"reflect"
    	"strings"
    
    	"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"
    	en_translations "github.com/go-playground/validator/v10/translations/en"
    	zh_translations "github.com/go-playground/validator/v10/translations/zh"
    )
    var trans ut.Translator
    type RegisterForm struct {
    	UserName   string `json:"username" binding:"required,min=3,max=10"`
    	Password   string `json:"password" binding:"required,min=3"`
    	RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
    }
    func removeTopStruct(fileds map[string]string) map[string]string {
    	rsp := map[string]string{}
    	for field, err := range fileds {
    		rsp[field[strings.Index(field, ".")+1:]] = err
    	}
    	return rsp
    }
    //定义翻译的方法
    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() //英文翻译器
    		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
    		uni := ut.New(enT, zhT, enT)
    		trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("uni.GetTranslator(%s)", locale)
    		}
    		switch locale {
    		case "en":
    			en_translations.RegisterDefaultTranslations(v, trans)
    		case "zh":
    			zh_translations.RegisterDefaultTranslations(v, trans)
    		default:
    			en_translations.RegisterDefaultTranslations(v, trans)
    		}
    		return
    	}
    	return
    }
    
    func main() {
    	if err := InitTrans("zh"); err != nil {
    		fmt.Println("初始化翻译器错误")
    		return
    	}
    	router := gin.Default()
    	router.POST("/register", func(c *gin.Context) {
    		var registerForm RegisterForm
    		if err := c.ShouldBind(&registerForm); err != nil {
    			fmt.Println(err.Error())
    			// 翻译错误
    			errs, ok := err.(validator.ValidationErrors)
    			if !ok {
    				c.JSON(http.StatusOK, gin.H{
    					"message": err.Error(),
    				})
    			}
    			c.JSON(http.StatusOK, gin.H{
    				"error": removeTopStruct(errs.Translate(trans)),
    			})
    			return
    		}
    		fmt.Println("注册的参数:", registerForm)
    		c.JSON(http.StatusOK, gin.H{
    			"code":    1,
    			"message": "成功",
    			"data": gin.H{
    				"username": registerForm.UserName,
    				"password": registerForm.Password,
    			},
    		})
    	})
    
    	router.Run(":8080")
    }
    
  • 7、可以将上面这块封装成一个公共方法,以便在别的地方使用

    package utils
    
    import (
    	"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"
    )
    
    var Trans ut.Translator
    
    func init() {
    	if err := InitTrans("zh"); err != nil {
    		fmt.Println("初始化翻译器错误")
    		panic("初始化翻译器错误")
    	}
    }
    
    // RemoveTopStruct 移除多余的标签
    func RemoveTopStruct(fileds map[string]string) map[string]string {
    	rsp := map[string]string{}
    	for field, err := range fileds {
    		rsp[field[strings.Index(field, ".")+1:]] = err
    	}
    	return rsp
    }
    
    // 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() //英文翻译器
    		//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
    		uni := ut.New(enT, zhT, enT)
    		Trans, ok = uni.GetTranslator(locale)
    		if !ok {
    			return fmt.Errorf("uni.GetTranslator(%s)", locale)
    		}
    		switch locale {
    		case "en":
    			enTranslations.RegisterDefaultTranslations(v, Trans)
    		case "zh":
    			zhTranslations.RegisterDefaultTranslations(v, Trans)
    		default:
    			enTranslations.RegisterDefaultTranslations(v, Trans)
    		}
    		return
    	}
    	return
    }
    
  • 8、外部使用

    package main
    
    import (
    	"fmt"
    	"gin_stuty/01/utils"
    	"github.com/gin-gonic/gin"
    	"github.com/go-playground/validator/v10"
    	"net/http"
    )
    
    type RegisterForm struct {
    	UserName   string `json:"username" binding:"required,min=3,max=10"`
    	Password   string `json:"password" binding:"required,min=3"`
    	RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
    }
    
    func main() {
    	router := gin.Default()
    	router.POST("/register", func(ctx *gin.Context) {
    		var registerForm RegisterForm
    		if err := ctx.ShouldBind(&registerForm); err != nil {
    			fmt.Println(err.Error())
    			// 翻译错误
    			errs, ok := err.(validator.ValidationErrors)
    			if !ok {
    				ctx.JSON(http.StatusOK, gin.H{
    					"message": err.Error(),
    				})
    			}
    			// 使用封装的翻译方法
    			ctx.JSON(http.StatusOK, gin.H{
    				"error": utils.RemoveTopStruct(errs.Translate(utils.Trans)),
    			})
    			return
    		}
    		ctx.JSON(http.StatusOK, gin.H{
    			"code":    0,
    			"message": "成功",
    		})
    	})
    	fmt.Println(fmt.Sprintf("服务已经启动:localhost:9000"))
    	router.Run(":9000")
    }
    

六、针对自定义错误翻译成中文

  • 1、回顾上面的自定义翻译器

    type RegisterForm1 struct {
    	//手机号码格式有规范可寻, 自定义validator,其中mobile是我们自定义的,官方是不带的
    	Mobile string `form:"mobile" json:"mobile" binding:"required,mobile"`
    }
    
    // ValidateMobile 自定义校验器
    func ValidateMobile(f validator.FieldLevel) bool {
    	// 获取字段的值
    	mobile := f.Field().String()
    	// 使用正则来校验
    	if ok, _ := regexp.MatchString(`^1[3,5,7,8]\d{9}$`, mobile); ok {
    		return true
    	} else {
    		return false
    	}
    }
    
  • 2、注册自定义校验器来验证

    func main() {
    	// 注册校验器
    	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    		_ = v.RegisterValidation("mobile", ValidateMobile)
    		_ = v.RegisterTranslation("mobile", utils.Trans, func(ut ut.Translator) error {
    			return ut.Add("mobile", "{0}手机号码不符合规则", true)
    		}, func(ut ut.Translator, fe validator.FieldError) string {
    			t, _ := ut.T("mobile", fe.Field())
    			return t
    		})
    	}
    	router := gin.Default()
    	router.POST("/register", func(ctx *gin.Context) {
    		var registerForm RegisterForm1
    		if err := ctx.ShouldBindJSON(&registerForm); err != nil {
    			fmt.Println(err.Error())
    			// 翻译错误
    			errs, ok := err.(validator.ValidationErrors)
    			if !ok {
    				ctx.JSON(http.StatusOK, gin.H{
    					"message": err.Error(),
    				})
    			}
    			// 使用封装的翻译方法
    			ctx.JSON(http.StatusOK, gin.H{
    				"error": utils.RemoveTopStruct(errs.Translate(utils.Trans)),
    			})
    			return
    		}
    		ctx.JSON(http.StatusOK, gin.H{
    			"code":    0,
    			"message": "成功",
    		})
    	})
    	router.Run(":9000")
    }
    
  • 3、一般自定义校验器会统一存放到项目的validator文件夹下,这里就不抽取出去

你可能感兴趣的:(go,golang)