目录
gin 的参数校验
gin 使用了 go-playground/validator库, 使用tag声明的方式, 支持http请求request中的各类校验
使用方法
1. 校验参数定义
校验参数是定义在结构体上的,因此在首先需要定义好接受数据的结构体,再定义每个属性的校验参数:
1 2 3 4 5 6 7 8 9 10 11 12 |
type Timeout struct { Connect int `binding:"omitempty,gte=1,lte=75"` Read int `binding:"omitempty,gte=1,lte=1000"` Send int `binding:"omitempty,gte=1,lte=1000"` } type MyTestStruct struct { Domains []string `binding:"gt=0,dive,required,min=1,max=100"` Name string `binding:"required"` UseTimeout bool Timeout Timeout `binding:"required_with=UseTimeout"` } |
结构体字段 binding
tag的内容就是校验参数。
2. 数据绑定
gin中提供了常用的 ShouldBindWith/ShouldBindUri
方法,来实现自动的数据与结构体的绑定,提供了以下类型的数据:
数据类型 | Tag | Context-Type | 数据源 | 使用示例 |
---|---|---|---|---|
JSON | json | application/json | Body | ShouldBindWith(&data, binding.Query) |
XML | xml | application/xml、text/xml | Body | ShouldBindWith(&data, binding.XML) |
Form | form | application/x-www-form-urlencoded | Body | c.ShouldBindWith(&data, binding.Form) |
Query | form | url query | ShouldBindWith(&data, binding.Query) | |
FormPost | form | application/x-www-form-urlencoded | Body | ShouldBindWith(&data, binding.FormPost) |
FormMultipart | form | multipart/form-data | Body | ShouldBindWith(&data, binding.FormMultipart) |
ProtoBuf | application/x-protobuf | Body | ShouldBindWith(&data, binding.ProtoBuf) | |
MsgPack | application/x-msgpack、application/msgpack | Body | ShouldBindWith(&data, binding.MsgPack) | |
YAML | application/x-yaml | Body | ShouldBindWith(&data, binding.YAML) | |
Uri | uri | uri | ShouldBindUri(&data) | |
Header | header | Header | ShouldBindWith(&forwardRule, binding.Header) |
说明:
数据类型:指的是传输的数据类型
Tag:结构体定义的Tag,揭示了数据字段与结构体字段的对应关系,如:Form类型数据中 id对应结构体中的ID字段,那么就需要定义一下内容:
1 2 3 4 5 |
type xxxx struct { ... ID int64 `form:"id"` ... } |
Context-Type:HTTP中的Header 字段之一,表示Body的数据类型,gin.Context
中的Bind
方法会根据Content-Type
自动绑定数据到对应类型上
1 2 3 4 |
func (c *Context) Bind(obj interface{}) error { b := binding.Default(c.Request.Method, c.ContentType()) // 获取数据类型 return c.MustBindWith(obj, b) // 校验数据 } |
数据源:指的是从哪里取数据,Body
指的是从HTTP Body中获取数据,url query
指从URL的quey参数中获取数据,url
指的是从url中获取参数,如:/api/v1/xxx/:id
,id参数就是从url中来获取的。
使用示例:使用的例程
3. 校验结果获取
ShouldBindWith
会返回error信息,如果error信息不为空,则表明出现错误,错误信息就包含在error中,如果error为空,则表明校验通过,示例如下:
1 2 3 4 5 6 7 8 9 |
var data MyTestStruct // 存储数据内容 // 获取 & 校验数据 if err = c.ShouldBindWith(&data, binding.JSON); err != nil { // 出现错误,打印错误内容 fmt.Println(err) return } // 校验成功,打印数据内容 fmt.Println(data) |
校验规则
完整的校验规则可参考https://godoc.org/github.com/go-playground/validator,下面分享常用的校验规则规则:
数字值限制
注:如果限制之间存在冲突,如
eq=10,ne=10
,则会根据先后顺序,后面的会覆盖前面的定义,以后面定义的为准;校验内容为eq=10,ne=10
,只会生效ne=10
,如果存在冲突,所有校验规则均符合此规律。
限制范围:
1 2 3 4 5 6 |
max=10 # 最大值为10,即小于等于10 min=10 # 最小值为10,即大于等于10 gt=10 # 大于 10 gte=10 # 大于等于10 lt=10 # 小于10 lte=10 # 小于等于10 |
限制值
1 2 3 4 |
eq=10 # 等于10 ne=10 # 不等于10 oneof=1 2 3 # 值只能是1、2 或 3 len=3 # 限制位数为3位数 |
字符串长度限制
限制长度范围
1 2 3 4 5 6 |
max=10 # 最大长度为10 min=10 # 最小长度为10 gt=10 # 长度大于10 lt=10 # 长度小于10 gte=10 # 长度大于等于10 let=10 # 长度小于等于10 |
限制值内容
1 2 3 4 |
eq=aaa # 值为aaa ne=aaa # 值不能为aaa oneof=a b c # 枚举,只能为a、b 或 c len=3 # 字符长度为3 |
存在性校验
注:这里需要特别注意的是,这里是否存在是根据0值与非0值判断的,如果字段中传了对应的0值,也会被认为是不存在的
必选
1 |
required # 必须 |
可选
1 |
omitempty,xxx=xxx # 可选 |
如果存在,则继续向后校验规则xxx=xxx
,如果不存在,则xxx=xxx
不生效,但是如果omitempty
之前存在校验规则,则前面的校验规则还是生效的,如 gte=-1,omitempty,len=3
,则gte=-1
规则始终生效,而len=3
只有在值不为0时生效。
关联校验
1 2 3 4 |
required_with=AAA # 当AAA存在时,此字段也必须存在 required_with_all=AAA BBB CCC # 当AAA BBB CCC 都存在时,此字段必须存在 required_without=AAA # 当AAA不存在时,此字段必须存在 required_without_all=AAA BBB # 当AAA BBB 都不存在时,此字段必须存在 |
结构体字段间校验
一个层级内部校验
1 2 3 4 5 6 |
eqfield=AAA # 和字段AAA值相等 nefield=AAA # 和字段AAA值不相等 gtfield=AAA # 大于字段AAA的值 gtefield=AAA # 大于等于字段AAA的值 ltfield=AAA # 小于字段AAA的值 ltefield=AAA # 小于等于AAA字段的值 |
多个层级之间校验
1 2 3 4 5 6 |
eqcsfield=AAA.B # 等于AAA中的B字段 necsfield=AAA.B # 不等于AAA中的B字段 gtcsfield=AAA.B # 大于AAA中的B字段 gtecsfield=AAA.B # 大于等于AAA中的B字段 ltcsfield=AAA.B # 小于AAA中的B字段 ltecsfield=AAA.B # 小于等于AAA中的B字段 |
注:此处要注意的是,多个结构体之间的校验必须是字段之间存在层级联系,且只能是上层的与下层的做关联,不能反过来,如下:
1 2 3 4 5 6 7 8 9 10 11 |
type Timeout struct { Connect int `json:"connect" validate:"required"` Read int `json:"read" validate:"required"` Send int `json:"send" validate:"required"` } type MyStruct struct { Name string `json:"name" validate:"required"` Out int `json:"out" validate:"eqcsfield=Timeout.Connect"` // 只能是Timeout同层级的去关联Timeout下的字段,不能Timeout下的字段关联上层字段 Timeout Timeout `json:"timeout"` } |
dive 对数组校验
dive 的意思是潜水,潜水也就是深入到水下,dive在这里的意思是也差不多,就是更深入一层的意思,如当前字段类型是 []string
,那么深入一层,就从 整体(array)
到 局部(array中的一个元素)
,对应到校验上,就是dive前是对整体校验,dive后是对整体中的元素校验。示例:
一般数组
1 |
Domains []string `binding:"gt=0,dive,required,min=1,max=100"` |
检验内容:[]string
长度必须大于0,数组中元素string长度必须在1-100之间。
结构体数组校验
1 |
Cors []AStruct `binding:"dive"` |
虽然这里dive
前后未定义相关校验规则,但是如果要启用 AStruct 中定义的校验规则,那么dive是必须的,否则AStruct
中定义的规则不会生效。
dive对Map校验
dive
的含义在这里仍然类似于数组,整体 -> 局部
的规则不变,但是map的特殊性在于不仅有value值,还有key值,这里就需要方法去区分是校验value,还是校验key;这里使用了 keys 和 endkeys来标记key值的校验范围,从keys开始,至endkeys结束。
1 |
ReqHeaders map[string]string `binding:"dive,keys,min=1,max=100,endkeys,required,min=1,max=100"` |
校验内容:未对整体做校验,限制key值长度必须在1-100之间,value值的长度也必须在1-100之间
structonly
当一个结构体定义了校验规则,但是在某些地方,不需要这些校验规则生效的时候,可以使用structonly
标记,存在此标记的结构体内部的校验规则将不会再生效。如下:
1 2 3 4 5 6 7 8 9 10 |
type Timeout struct { Connect int `json:"connect" binding:"required"` Read int `json:"read" binding:"required"` Send int `json:"send" binding:"required"` } type MyStruct struct { Name string `json:"name" binding:"required"` Timeout Timeout `json:"timeout" binding:"structonly"` } |
结构体Timeout
各个字段定义了校验内容,但是我在MyStruct
的Timeout
字段使用了structonly
标记,那么Timeout
中定义的校验内容将不再生效。
nostructlevel
nostructlevel 类似于structonly,唯一不同之处是要想忽略规则的效果生效,必须要使用required
或 omitempty
。
1 2 3 4 5 6 7 8 9 10 11 |
type Timeout struct { Connect int `json:"connect" binding:"required,gte=1,lte=1000"` Read int `json:"read" binding:"gte=1,lte=1000"` Send int `json:"send" binding:"gte=1,lte=1000"` } type MyStruct struct { Name string `form:"name" json:"name" binding:"required"` UseMeta bool `json:"use_meta"` Timeout Timeout `json:"timeout" binding:"required,nostructlevel"` } |
忽略校验规则
1 |
- # 忽略字段校验规则,常用于嵌套结构中,用来忽略嵌套结构下的校验规则 |
多校验规则“或”运算
1 |
| # 多个检验规则默认之间是与关系,使用 | 可实现多运算规则之间进行或运算 |
ShouldBind与Bind方法的区别
Gin参数验证shouldbind/Bind区别及多次绑定 request body
几种常用参数校验
get 参数
来源:gin: Only Bind Query String
curl -X GET “localhost:8085/testing?name=eason&address=xyz”
1 2 3 4 5 6 7 8 9 10 11 |
type Person struct { Name string `form:"name"` Address string `form:"address"` } var person Person if c.ShouldBindQuery(&person) == nil { log.Println("====== Only Bind By Query String ======") log.Println(person.Name) log.Println(person.Address) } |
路径参数
curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package main import "github.com/gin-gonic/gin" type Person struct { ID string `uri:"id" binding:"required,uuid"` Name string `uri:"name" binding:"required"` } func main() { route := gin.Default() route.GET("/:name/:id", func(c *gin.Context) { var person Person if err := c.ShouldBindUri(&person); err != nil { c.JSON(400, gin.H{"msg": err}) return } c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) }) route.Run(":8088") } |
json body
来源:gin: Model binding and validation
1 2 3 4 5 6 7 8 9 10 |
type Login struct { User string `json:"user" binding:"required"` Password string `json:"password" binding:"required"` } var json Login if err := c.ShouldBindJSON(&json); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } |
header
curl -H “rate:300” -H “domain:music” 127.0.0.1:8080/
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 |
package main import ( "fmt" "github.com/gin-gonic/gin" ) type testHeader struct { Rate int `header:"Rate"` Domain string `header:"Domain"` } func main() { r := gin.Default() r.GET("/", func(c *gin.Context) { h := testHeader{} if err := c.ShouldBindHeader(&h); err != nil { c.JSON(200, err) } fmt.Printf("%#v\n", h) c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain}) }) r.Run() } |
json body是一个对象数组
ShouldBindJSON
并不会校验对象数组中的每个对象是否符合要求, 需要自行调用方法处理
可以抽象出一个通用的函数
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 |
var ErrNotArray = errors.New("validate array fail, only support array") func ToSlice(array interface{}) ([]interface{}, error) { v := reflect.ValueOf(array) if v.Kind() != reflect.Slice { return nil, ErrNotArray } l := v.Len() ret := make([]interface{}, l) for i := 0; i < l; i++ { ret[i] = v.Index(i).Interface() } return ret, nil } func ValidateArray(data interface{}) (bool, string) { array, err := ToSlice(data) if err != nil { return false, err.Error() } if len(array) == 0 { return false, "the array should contain at least 1 item" } for index, item := range array { if err := binding.Validator.ValidateStruct(item); err != nil { message := fmt.Sprintf("data in array[%d], %s", index, ValidationErrorMessage(err)) return false, message } } return true, "valid" } |
使用时
1 2 3 4 5 6 7 8 9 10 |
var body []action if err := c.ShouldBindJSON(&body); err != nil { // bad request return } if valid, message := common.ValidateArray(body); !valid { // bad request: message return } |
更友好的错误提示
当validation报错的时候, 我们期望得到一个更友好的提示信息, 便于使用者确认问题
参考:
- https://github.com/gin-gonic/gin/issues/430
- https://medium.com/@seb.nyberg/better-validation-errors-in-go-gin-88f983564a3d
以下是一种实现, 处理的常用的 validation 规则的错误展示
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 |
package util import ( "fmt" "io" "github.com/go-playground/validator/v10" log "github.com/sirupsen/logrus" ) // 这里是通用的 FieldError 处理, 如果需要针对某些字段或struct做定制, 需要自行定义一个 type ValidationFieldError struct { Err validator.FieldError } func (v ValidationFieldError) String() string { e := v.Err switch e.Tag() { case "required": return fmt.Sprintf("%s is required", e.Field()) case "max": return fmt.Sprintf("%s cannot be longer than %s", e.Field(), e.Param()) case "min": return fmt.Sprintf("%s must be longer than %s", e.Field(), e.Param()) case "email": return "Invalid email format" case "len": return fmt.Sprintf("%s must be %s characters long", e.Field(), e.Param()) case "gt": return fmt.Sprintf("%s must greater than %s", e.Field(), e.Param()) case "gte": return fmt.Sprintf("%s must greater or equals to %s", e.Field(), e.Param()) case "lt": return fmt.Sprintf("%s must less than %s", e.Field(), e.Param()) case "lte": return fmt.Sprintf("%s must less or equals to %s", e.Field(), e.Param()) case "oneof": return fmt.Sprintf("%s must be one of '%s'", e.Field(), e.Param()) } return fmt.Sprintf("%s is not valid, condition: %s", e.Field(), e.ActualTag()) } func ValidationErrorMessage(err error) string { if err == io.EOF { return "EOF, json decode fail" } validationErrs, ok := err.(validator.ValidationErrors) if !ok { message := fmt.Sprintf("json decode or validate fail, err=%s", err) log.Info(message) return message } // currently, only return the first error for _, fieldErr := range validationErrs { return ValidationFieldError{fieldErr}.String() } return "validationErrs with no error message" } |
判断: 空值还是没有传递
有些场景, 如果一个json对象中, 某个字段可以为空, 但是非空的时候, 要执行某些特殊的逻辑
此时, 使用默认的validation是无法判断, 请求中没有这个字段, 还是传了空值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// validate var body action err := c.ShouldBindBodyWith(&body, binding.JSON) if err != nil { // bad request, ValidationErrorMessage(err) return } var data map[string]interface{} err = c.ShouldBindBodyWith(&data, binding.JSON) if err != nil { // bad request, ValidationErrorMessage(err) return } if _, ok := data["name"]; !ok { // do something } |