这一篇介绍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中只需要关注账号密码是否正确就可以,无需关注于参数错误的处理过程。

对应的代码参见:https://github.com/harleylau/myGin/tree/master/v0.4