乐趣区

关于golang:学会gin框架的参数验证之validator库看这一篇就够用了

前言

哈喽,大家好,我是 asong。这是我的第十篇原创文章。这周在公司做我的项目,在做 API 局部开发时,须要对申请参数的校验,避免用户的歹意申请。例如日期格局,用户年龄,性别等必须是失常的值,不能随便设置。最开始在做这一部分的时候,我采纳老办法,本人编写参数检验办法,对立进行参数验证。起初在共事 CR 的时候,说 GIN 有更好的参数检验办法,gin 框架应用 github.com/go-playground/validator 进行参数校验,咱们只须要在定义构造体时应用 bindingvalidatetag 标识相干校验规定,就能够进行参数校验了,很不便。置信也有很多小伙伴不晓得这个性能,明天就来介绍一下这部分。

本人翻译了一份 gin 官网中文文档。关注公众号[Golang 梦工厂](扫描下方二维码),后盾回复:gin,即可获取。

疾速装置

应用之前,咱们先要获取 validator 这个库。

# 第一次装置应用如下命令 $ go get github.com/go-playground/validator/v10# 我的项目中引入包 import "github.com/go-playground/validator/v10"

简略示例

装置还是很简略的,上面我先来一个官网样例,看看是怎么应用的,而后开展剖析。

package mainimport ("fmt" "net/http" "github.com/gin-gonic/gin")type RegisterRequest struct {Username string `json:"username" binding:"required"` Nickname string `json:"nickname" binding:"required"` Email    string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required"` Age      uint8  `json:"age" binding:"gte=1,lte=120"`}func main() { router := gin.Default() router.POST("register", Register) router.Run(":9999")}func Register(c *gin.Context) {var r RegisterRequest err := c.ShouldBindJSON(&r) if err != nil {fmt.Println("register failed")  c.JSON(http.StatusOK, gin.H{"msg": err.Error()})  return } // 验证 存储操作省略..... fmt.Println("register success") c.JSON(http.StatusOK, "successful")}
  • 测试
curl --location --request POST 'http://localhost:9999/register' \--header 'Content-Type: application/json' \--data-raw '{"username":"asong","nickname":"golang 梦工厂 ","email":"7418.com","password":"123","age": 140}'
  • 返回后果
{"msg": "Key:'RegisterRequest.Email'Error:Field validation for'Email'failed on the'email'tag\nKey:'RegisterRequest.Age'Error:Field validation for'Age'failed on the'lte'tag"}

看这个输入后果,咱们能够看到 validator 的测验失效了,email 字段不是一个非法邮箱,age 字段超过了最大限度。咱们只在构造体中增加 tag 就解决了这个问题,是不是很不便,上面咱们就来学习一下具体应用。

validator 库

gin 框架是应用 validator.v10 这个库来进行参数验证的,所以咱们先来看看这个库的应用。

先装置这个库:

$ go get github.com/go-playground/validator/v10

而后先写一个简略的示例:

package mainimport ("fmt" "github.com/go-playground/validator/v10")type User struct {Username string `validate:"min=6,max=10"` Age      uint8  `validate:"gte=1,lte=10"` Sex      string `validate:"oneof=female male"`}func main() { validate := validator.New() user1 := User{Username: "asong", Age: 11, Sex: "null"} err := validate.Struct(user1) if err != nil {fmt.Println(err) } user2 := User{Username: "asong111", Age: 8, Sex: "male"} err = validate.Struct(user2) if err != nil {fmt.Println(err) }}

咱们在构造体定义 validator 标签的 tag,应用 validator.New() 创立一个验证器,这个验证器能够指定选项、增加自定义束缚,而后在调用他的 Struct() 办法来验证各种构造对象的字段是否合乎定义的束缚。

下面的例子,咱们在 User 构造体中,有三个字段:

  • Name:通过 min 和 max 来进行束缚,Name 的字符串长度为 [6,10] 之间。
  • Age:通过 gte 和 lte 对年老的范畴进行束缚,age 的大小大于 1,小于 10。
  • Sex:通过 oneof 对值进行束缚,只能是所列举的值,oneof 列举出性别为男士???? 和女士????(不是硬性规定奥,可能还有别的性别)。

所以 user1 会进行报错,错误信息如下:

Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tagKey: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tagKey: 'User.Sex' Error:Field validation for 'Sex' failed on the 'oneof' tag

各个字段违反了什么束缚,一眼咱们便能从错误信息中看进去。看完了简略示例,上面我就来看一看都有哪些 tag,咱们都能够怎么应用。本文不介绍所有的 tag,更多应用办法,请到官网文档自行学习。

字符串束缚

  • excludesall:不蕴含参数中任意的 UNICODE 字符,例如excludesall=ab
  • excludesrune:不蕴含参数示意的 rune 字符,excludesrune=asong
  • startswith:以参数子串为前缀,例如startswith=hi
  • endswith:以参数子串为后缀,例如endswith=bye
  • contains=:蕴含参数子串,例如contains=email
  • containsany:蕴含参数中任意的 UNICODE 字符,例如containsany=ab
  • containsrune:蕴含参数示意的 rune 字符,例如 `containsrune=asong;
  • excludes:不蕴含参数子串,例如excludes=email

范畴束缚

范畴束缚的字段类型分为三种:

  • 对于数值,咱们则能够束缚其值
  • 对于切片、数组和 map,咱们则能够束缚其长度
  • 对于字符串,咱们则能够束缚其长度

罕用 tag 介绍:

  • ne:不等于参数值,例如ne=5
  • gt:大于参数值,例如gt=5
  • gte:大于等于参数值,例如gte=50
  • lt:小于参数值,例如lt=50
  • lte:小于等于参数值,例如lte=50
  • oneof:只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号突围,例如oneof=male female
  • eq:等于参数值,留神与 len 不同。对于字符串,eq束缚字符串自身的值,而 len 束缚字符串长度。例如eq=10
  • len:等于参数值,例如len=10
  • max:小于等于参数值,例如max=10
  • min:大于等于参数值,例如min=10

Fields 束缚

  • eqfield:定义字段间的相等束缚,用于束缚同一构造体中的字段。例如:eqfield=Password
  • eqcsfield:束缚对立构造体中字段等于另一个字段(绝对),确认明码时能够应用,例如:eqfiel=ConfirmPassword
  • nefield:用来束缚两个字段是否雷同,确认两种色彩是否统一时能够应用,例如:nefield=Color1
  • necsfield:束缚两个字段是否雷同(绝对)

罕用束缚

  • unique:指定唯一性束缚,不同类型解决不同:

    • 对于 map,unique 束缚没有反复的值
    • 对于数组和切片,unique 没有反复的值
    • 对于元素类型为构造体的碎片,unique 束缚构造体对象的某个字段不反复,应用 unique=field 指定字段名
  • email:应用 email 来限度字段必须是邮件模式,间接写 eamil 即可,无需加任何指定。
  • omitempty:字段未设置,则疏忽
  • -:跳过该字段,不测验;
  • |:应用多个束缚,只须要满足其中一个,例如rgb|rgba
  • required:字段必须设置,不能为默认值;

好啦,就介绍这些罕用的束缚,更多束缚学习请到文档自行学习吧,都有 example 供你学习,很快的。

gin 中的参数校验

学习了 validator,咱们也就晓得了怎么在 gin 中应用参数校验了。这些束缚是都没有变的,在 validator 中,咱们间接构造体中将束缚放到validate tag 中,同样情理,在 gin 中咱们只需将束缚放到bindingtag 中就能够了。是不是很简略。

然而有些时候,并不是所有的参数校验都能满足咱们的需要,所以咱们能够定义本人的束缚。自定义束缚反对自定义构造体校验、自定义字段校验等。这里来介绍一下自定义构造体校验。

自定义构造体校验

当波及到一些简单的校验规定,这些已有的校验规定就不能满足咱们的需要了。例如当初有一个需要,存在 db 的用户信息中创立工夫与更新工夫都要大于某一时间,假如是从前端传来的(当然不可能,哈哈)。当初咱们来写一个简略示例,学习一下怎么对这个参数进行校验。

package mainimport ("fmt" "net/http" "time" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10")type Info struct {CreateTime time.Time `form:"create_time" binding:"required,timing" time_format:"2006-01-02"` UpdateTime time.Time `form:"update_time" binding:"required,timing" time_format:"2006-01-02"`}// 自定义验证规定断言 func timing(fl validator.FieldLevel) bool {if date, ok := fl.Field().Interface().(time.Time); ok {today := time.Now()  if today.After(date) {return false} } return true}func main() { route := gin.Default() // 注册验证 if v, ok := binding.Validator.Engine().(*validator.Validate); ok {err := v.RegisterValidation("timing", timing)  if err != nil {fmt.Println("success")  } } route.GET("/time", getTime) route.Run(":8080")}func getTime(c *gin.Context) {var b Info // 数据模型绑定查问字符串验证 if err := c.ShouldBindWith(&b, binding.Query); err == nil {c.JSON(http.StatusOK, gin.H{"message": "time are valid!"}) } else {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) }}

写好了,上面我就来测试验证一下:

$ curl "localhost:8080/time?create_time=2020-10-11&update_time=2020-10-11"# 后果{"message":"time are valid!"}%$ curl "localhost:8080/time?create_time=1997-10-11&update_time=1997-10-11"# 后果{"error":"Key:'Info.CreateTime'Error:Field validation for'CreateTime'failed on the'timing'tag\nKey:'Info.UpdateTime'Error:Field validation for'UpdateTime'failed on the'timing'tag"}%

这里咱们看到尽管参数验证胜利了,然而这里返回的谬误显示的也太全了,在我的项目开发中不能够给前端返回这么具体的信息的,所以咱们须要革新一下:

func getTime(c *gin.Context) {var b Info // 数据模型绑定查问字符串验证 if err := c.ShouldBindWith(&b, binding.Query); err == nil {c.JSON(http.StatusOK, gin.H{"message": "time are valid!"}) } else {_, ok := err.(validator.ValidationErrors)  if !ok {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})   return  }  c.JSON(http.StatusOK, gin.H{"code": 1000, "msg": "param is error"}) }}

这里在呈现谬误时返回固定谬误即可。这里你也能够应用一个办法封装一下,对谬误进行解决在进行返回,更多应用办法等你察觉哟。

小彩蛋

咱们返回谬误时都是英文的,当谬误很长的时候,对于我这种英语渣渣,就要借助翻译软件了。所以要是能返回的谬误间接是中文的就好了。validator库自身是反对国际化的,借助相应的语言包能够实现校验谬误提示信息的主动翻译。上面就写一个代码演示一下啦。

package mainimport ("fmt" "log" "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" chTranslations "github.com/go-playground/validator/v10/translations/zh")var trans ut.Translator// loca 通常取决于 http 申请头的 'Accept-Language'func transInit(local string) (err error) {if v, ok := binding.Validator.Engine().(*validator.Validate); ok {zhT := zh.New() //chinese  enT := en.New() //english  uni := ut.New(enT, zhT, enT)  var o bool  trans, o = uni.GetTranslator(local)  if !o {return fmt.Errorf("uni.GetTranslator(%s) failed", local)  }  //register translate  // 注册翻译器  switch local {case "en":   err = enTranslations.RegisterDefaultTranslations(v, trans)  case "zh":   err = chTranslations.RegisterDefaultTranslations(v, trans)  default:   err = enTranslations.RegisterDefaultTranslations(v, trans)  }  return } return}type loginRequest struct {Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required,max=16,min=6"`}func main() { if err := transInit("zh"); err != nil {fmt.Printf("init trans failed, err:%v\n", err)  return } router := gin.Default() router.POST("/user/login", login) err := router.Run(":8888") if err != nil {log.Println("failed") }}func login(c *gin.Context) {var req loginRequest if err := c.ShouldBindJSON(&req); 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 } //login 操作省略 c.JSON(http.StatusOK, gin.H{  "code": 0,  "msg":  "success",})}

我这里申请参数中限度明码的长度,来验证一下吧。

curl --location --request POST 'http://localhost:8888/user/login' \--header 'Content-Type: application/json' \--data-raw '{"username":"asong","password":"11122222222222222222"}'# 返回{"msg": {        "loginRequest.Password": "Password 长度不能超过 16 个字符"}}

看,间接显示中文了,是不是很棒,咱们能够在测试的时候应用这个,上线我的项目不倡议应用呦!!!

总结

好啦,这一篇文章到这里完结啦。这一篇干货还是满满的。学会这些知识点,进步咱们的开发效率,省去了一些没必要写的代码。能用的轮子咱们还是不要错过滴。

我是 asong,一名普普通通的程序猿,让我一起缓缓变强吧。欢送各位的关注,咱们下期见~~~ 举荐往期文章:

  • 据说你还不会 jwt 和 swagger- 饭我都不吃了带着实际我的项目我就来了
  • 把握这些 Go 语言个性,你的程度将进步 N 个品位(二)
  • go 实现多人聊天室,在这里你想聊什么都能够的啦!!!
  • grpc 实际 - 学会 grpc 就是这么简略
  • go 规范库 rpc 实际
  • 2020 最新 Gin 框架中文文档 asong 又捡起来了英语,用心翻译
  • 基于 gin 的几种热加载形式
退出移动版