让 Gin 参数校验变得简单快捷 | 青训营

200 阅读3分钟

参数获取与校验

Gin 框架中,可以通过 binding 包进行参数获取与校验。binding 是一个非常好用的反序列化库,可以将 HTTP 请求体中的 JSON、XML、FormData 等格式的数据和 URL 上路径参数等绑定到设定好的 Go 结构体指针上,而且还提供了 go-playground/validator 库进行简单的参数校验。

gin bind 方法分为 BindXXXShouldBindXXX 两种。 BindXXX 方法在参数校验错误时,会先自动向返回信息中写入 400 的错误码再返回错误由服务器处理。而 ShouldBindXXX 方法不会写入错误码,而是直接放回错误完全由服务器处理。

以下是我针对一般的 URL 路径参数和 FormData 的请求体格式进行参数解析的代码示例。 如果每个方法入参不同,大可以用这种临时结构体,减少代码复杂度。

// Get URL Params
body := struct {
   UserID int64  `form:"user_id"
   Token  string `form:"token"
}{}
if err := c.ShouldBindQuery(&body); err != nil {
   c.JSON(http.StatusBadRequest, gin.H{"errMsg": err.Error()})
   return 
}

// Get formData Params
body := struct {
   UserID int64  `form:"user_id" json:"user_id"
   Token  string `form:"token" json:"token"
}{}
if err := c.ShouldBind(&body); err != nil {
   c.JSON(http.StatusBadRequest, gin.H{"errMsg": err.Error()})
   return 
}

通过这种方式获取入参可以免掉一些参数校验的业务逻辑。这里通过 binding 包可以便捷的进行更多的入参校验。 binding 也是通过结构体中的 Tag 对字段进行校验,简单的代码示例如下:

ActionType  int    `form:"action_type" json:"action_type" binding:"required,oneof=1 2"`

该字段检测入参时有三种引发 BindXXX/ShouldBindXXX 方法返回错误的可能性

  • required 会校验该入参是否存在,如果不存在的话会返回错误
  • oneof 会校验入参的值,如果不满足 oneof 给出的值会返回错误
  • int 如果入参无法转换为 int 类型也会返回错误

实际上 binding 包能检测的入参类型更多,常见的有

  • string
    • email:验证通用正则表达式验证电子邮箱
    • e164:验证国际 E.164 标准验证电话
    • iso3166_1_alpha2:验证 ISO-3166-1 两字母标准验证国家代码
    • uppercase:只允许大写字母
    • lowercase:只允许小写字母
    • contains:包含指定子串
    • alphanum:只允许包含英文字母和数字
    • alpha:只允许包含英文字母
    • endswith:字符串以指定子串结尾
    • startwith:字符串以指定子串开始
    • gt:大于设定的长度
    • gte:大于等于设定的长度
    • lt:小于设定的长度
    • lte:小于等于设定的长度
  • int
    • oneof:只能为预设的值
    • gt:大于设定的值
    • gte:大于等于设定的值
    • lt:小于设定的值
    • lte:小于等于设定的值
    • max:最大值
    • min:最小值
    • ne:不等于

当入参不满足输入条件时, BindXXX/ShouldBindXXX 方法会返回validator.ValidationErrors 错误类型,该类型实现了 error 接口。如果需要定义返回的错误信息,可以通过 switch (type) 语法处理错误。

深入该包中我们可以看到 type ValidationErrors []FieldError,即该方法返回的是一个自定义类型的数组。深入探究 FieldError 类型:

// FieldError contains all functions to get error details
type FieldError interface {
   Tag() string
   ActualTag() string
   Namespace() string
   StructNamespace() string
   Field() string
   StructField() string
   Value() interface{}
   Param() string
   Kind() reflect.Kind
   Type() reflect.Type
   Translate(ut ut.Translator) string
   Error() string
}

可以看到该类型是一个接口,这里省略了方法的注释,主要注意到 FieldTag 字段,Field 方法返回字段名, Tag 方法返回引发错误的 Tag名。以下为我手动处理错误的方法:

func HandleBindError(err error) error {
   switch err.(type) {
   case validator.ValidationErrors:
      firstErr := err.(validator.ValidationErrors)[0]
      switch firstErr.Tag() {
      case "required":
         return ParamEmptyError
      case "oneof":
         return ParamUnknownActionTypeError
      case "lte":
         return ParamInputLengthExceededError
      default:
         fmt.Printf("%s\n", firstErr.Tag())
         dyerr := UnknownError
         dyerr.ErrMessage = firstErr.Error()
         return dyerr
      }
   case *strconv.NumError:
      return ParamInputTypeError
   default:
      fmt.Printf("%T\n", err)
      dyerr := UnknownError
      dyerr.ErrMessage = err.Error()
      return dyerr
   }
}

为已知错误自定义一个错误结构体,为未知错误输出错误日志并返回未知类型。在测试中可以不断校准。