跟Gin一块搭建自己的web框架(六)
这一篇介绍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中只需要关注账号密码是否正确就可以,无需关注于参数错误的处理过程。