这一篇介绍gin中的绑定和数据验证。
不管是在query中,还是在body中,如果要一个一个的去获取参数并放入对应的变量中,是一个比较繁琐的过程,gin里边提供了一个自动绑定的方法,能够将query或者body中的参数方便的放入到我们定义的struct中。
同时在绑定参数的时候,我们也能够指定参数的范围或者特性,对参数进行验证。
所以将绑定和参数验证放在一块讲,接下去实现一个比较简单的绑定功能和参数验证。
参数绑定
func (c *Context) EnsureBody(item interface{}) bool { if err := c.ParseBody(item); err != nil { c.Fail(400, err) return false } return true}// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer.func (c *Context) ParseBody(item interface{}) error { decoder := json.NewDecoder(c.Req.Body) if err := decoder.Decode(&item); err == nil { return Validate(c, item) } else { return err }}复制代码
我们用一个EnsureBody的函数来解析请求包中的body,它其实是调用了一个ParseBody的函数。这个函数会将req.Body解析到item中,如果解析失败,说明参数不符合我们的要求,返回错误。如果解析成功,就是将参数绑定到了我们定义的结构体中,接下去调用Validate对参数的有效性进行验证。
参数验证
首先说一下validate参数验证的好处。假设一个最简单的场景, 用户登录的时候必须要传账户名和密码,我们先定义这样的结构体
// LoginJSON .type LoginJSON struct { User string `json:"user"` Password string `json:"password"`}复制代码
在从请求中接受参数之后,必然需要用if语句去判断参数中是否确实带了User参数和Password参数,这个在单一的请求中还算好办,两条if语句就解决了。但是如果有很多类似的请求,要验证其他的结构体,或者结构体中有很多的参数需要验证,这个就需要一堆的if语句,在代码简洁性上就会大打折扣。
但是如果有了validate模块,就能够解放劳动力,我们只需要把参数的要求在tag中写清楚,具体的验证过程全都可以交给validate模块去做。比如登录模块User和Password必须携带,就可以这么定义结构体
// LoginJSON .type LoginJSON struct { User string `json:"user" binding:"required"` Password string `json:"password" binding:"required"`}复制代码
用一个required来限定必须携带的参数,再比如注册的例子,在gin中可以这么定义结构体
type RegisterReq struct { // 字符串的 gt=0 表示长度必须 > 0,gt = greater than Username string `validate:"gt=0"` // 同上 PasswordNew string `validate:"gt=0"` // eqfield 跨字段相等校验 PasswordRepeat string `validate:"eqfield=PasswordNew"`}复制代码
注册的时候限定了UserName的长度和Password的长度,并且两次数据的密码必须一致。我们只需要关注参数的定义逻辑就可以,无需关注请求中的参数验证逻辑,validate都帮我们做了。
当然这个需要很复杂的validate模块,这边只是简单的实现validate中required的功能。
从结构上来说,如果我们自定义的struct中子struct,那么它其实有点类似一棵树的结构,在验证的时候就需要对整棵树进行验证,这个就涉及到树的遍历。遍历一棵树无非就是广度优先或者深度优先,在gin中采用了深度优先的方式。
for i := 0; i < typ.NumField(); i++ { field := typ.Field(i) fieldValue := val.Field(i).Interface() zero := reflect.Zero(field.Type).Interface() // Validate nested and embedded structs (if pointer, only do so if not nil) if field.Type.Kind() == reflect.Struct || (field.Type.Kind() == reflect.Ptr && !reflect.DeepEqual(zero, fieldValue)) { err = Validate(c, fieldValue) } if strings.Index(field.Tag.Get("binding"), "required") > -1 { if reflect.DeepEqual(zero, fieldValue) { name := field.Name if j := field.Tag.Get("json"); j != "" { name = j } else if f := field.Tag.Get("form"); f != "" { name = f } err = errors.New("Required " + name) c.Error(err, "json validation") } } }复制代码
在validate模块中,循环遍历所有的字段,如果字段是一个指针指向另一个结构体,那么采用深度优先的方法,验证这个子结构体的合法性。如果参数的tag中,带有required限制,验证这个参数是否确实有值。
main函数修改
在上一篇的基础之上,为了验证参数绑定功能,增加一个账号密码登录功能
v1 := r.Group("/v1"){ v1.POST("/pwlogin", v1PasswordLoginfunc)}复制代码
对应的handler如下
func v1PasswordLoginfunc(c *gin.Context) { var json LoginJSON if c.EnsureBody(&json) { if json.User == "harleylau" && json.Password == "password" { c.JSON(200, gin.H{ "status": "you are logged in"}) } else { c.JSON(401, gin.H{ "status": "unauthorized"}) } }}复制代码
将参数解析验证之后,在handler中只需要关注账号密码是否正确就可以,无需关注于参数错误的处理过程。
对应的代码参见: