13

Gin框架系列04:趣谈参数绑定与校验

 4 years ago
source link: http://www.cnblogs.com/pingyeaa/p/12674589.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

导读

在第二节,我们学习了Gin框架的路由定义与参数接收,今天应一位同学的要求,来讲解一下参数的绑定与校验。

为什么校验参数?

本不必抛出这个问题的,但顾及到初出茅庐的同学,这里解释一下。

je6jUjm.jpg!web

假设做一个注册接口,传过来的用户名是不是不能太骚气?比如一堆空格和符号之类的;密码是不是不能太长也不能太短?手机号是不是要符合规则?性别是不是不能填人妖?

MzMZvej.png!web

另外,登录的时候我们也需要验证账号密码是不是正确的,那么为了方便上手,咱就先来个简单示例,做登录验证。

激情演示

做登录之前得先想清楚需要对用户名密码做什么样的限制,比如他们都不能为空、用户名只能是字母或数字、密码长度只能在6位到12位之间等,如果各位看官没有异议,接下来我就拿上述的这几个条件来演示了。

ZJrAFrM.png!web

定义结构体与接口

首先得有个地方存接收到的用户名、密码参数,那就定一个名叫 Login 的结构体吧。

type Login struct {
	User     string
	Password string
}

然后再定义一个登录接口,这块不知道啥意思的同学可以去看我的第二篇教程。

router.POST("/login", func(c *gin.Context) {

})

接口是定了,怎么才能把接收的参数放到 Login 结构体里去呢?

6JvaQvN.png!web

绑定参数

我们去翻一下 gin.Context 下面都有些什么好东西可以拿来用。看到了一个叫做 Bind 的东东,官方说它可以自动根据 Content-Type 设置的值解析请求过来的参数,然后把参数设置到结构体里。

func (c *Context) Bind(obj interface{}) error {
	b := binding.Default(c.Request.Method, c.ContentType())
	return c.MustBindWith(obj, b)
}

哇塞,这就有意思了,调用一下试试。记得把 Login 的引用传过去,毕竟人家还要赋值的。

router.POST("/login", func(c *gin.Context) {
  var login Login
  c.Bind(&login)
  c.JSON(200, login)
})

果不其然,跑起来的同学应该可以发现,它失败了,并没有取到我想要的参数值。

curl -d "user=pingye&password=123" http://localhost:8080/login
{"User":"","Password":""}

这不对啊,不是说好做彼此的天使吗?

FVbEfyI.png!web

不行,我要一层一层剥开 gin 框架是怎么处理的。

M3ANFz6.png!web

绑定失败,剖析源码

经过长达60分钟的精心研究,我终于发现了终极奥义,先把我绘制的图贴上。

FZJN7rR.png!web

事情是这样的,我们调用的 Bind 方法实际调用了两个方法 binding.Defaultc.MustBindWith ,前者的主要作用是根据终端请求的 Content-Type 选择处理器,没办法,gin太强,支持的类型太多了。我们刚才的请求方式被理所应当的分配给了formBinding。

var (
	JSON          = jsonBinding{}
	XML           = xmlBinding{}
	Form          = formBinding{}
	Query         = queryBinding{}
	FormPost      = formPostBinding{}
	FormMultipart = formMultipartBinding{}
	ProtoBuf      = protobufBinding{}
	MsgPack       = msgpackBinding{}
	YAML          = yamlBinding{}
	Uri           = uriBinding{}
	Header        = headerBinding{}
)

然后呢?在拿到了formBinding之后,就来到了c.MustBindWith方法,它的作用就是调用formBinding的Bind方法,原来这哥们就是个中间商在赚差价。

func (formBinding) Bind(req *http.Request, obj interface{}) error {
	if err := req.ParseForm(); err != nil {
		return err
	}
	if err := req.ParseMultipartForm(defaultMemory); err != nil {
		if err != http.ErrNotMultipart {
			return err
		}
	}
	if err := mapForm(obj, req.Form); err != nil {
		return err
	}
	return validate(obj)
}

Bind方法主要就干了两件事情,第一是解析传过来的表单参数,第二是找到结构体里的tag form 进行匹配赋值。看到这里我就明白了,原来只需要在 Login 后面加上一个tag。

继续绑定参数

那我们加上tag试一下。

type Login struct {
	User     string `form:"user"`
	Password string `form:"password"`
}

跑起来果然很完美。

curl -d "user=pingye&password=123" http://localhost:8080/login
{"User":"pingye","Password":"123"}

看到了这里,聪明的你应该涌出了很多想法,刚才说支持那么多类型,前端传的是json咋搞呢?同学们可以自己试一下,现有的这套代码啥都不用改就可以解析json,因为jsonBinding并没有去Login结构体找tag,所以不用在后面加上 json:"user" 的标识。

curl -H "Content-Type:application/json" -d '{"user":"pingye","password":"123455"}' http://localhost:8080/login
{"User":"pingye","Password":"123455"}

至于其他的类型,同学们可以自己去动手试验一下,我们必须得到参数验证环节了。

参数验证

OK,这就到了激动人心的参数验证时刻了,再回顾一下刚才的需求,用户名和密码不能为空,用户名只能是英文和数字,密码长度必须得在6到12位。

gin官方给出的示例是直接在tag中加校验规则,比如不能为空,就加上 binding:"required"

type Login struct {
	User     string `form:"user" binding:"required"`
	Password string `form:"password" binding:"required"`
}

在验证的地方也做一下处理,Bind会自动帮我们进行校验,如果校验失败会返回一个error,我们把它输出即可。

router.POST("/login", func(c *gin.Context) {
  var login Login
  err := c.Bind(&login)
  if err != nil {
    c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    return
  }
  c.JSON(200, login)
})

放心,参数验证的演示不会就这么结束的。

i6bQbq3.png!web

鉴于官方文档给的信息太少了,我们还是通过源码去找更多线索吧,通过源代码可以看到,gin的参数验证实际上并不是自己实现的,而是使用了一个叫做 go-playground/validator 的库。

.
├── LICENSE
├── Makefile
├── README.md
├── _examples
├── baked_in.go
├── benchmarks_test.go
├── cache.go
├── doc.go
├── errors.go
├── field_level.go
├── go.mod
├── go.sum
├── logo.png
├── non-standard
├── regexes.go
├── struct_level.go
├── testdata
├── translations
├── translations.go
├── util.go
├── validator.go
├── validator_instance.go
└── validator_test.go

4 directories, 19 files

里面有一个叫做 doc.go 的文件,有非常多的示例与解释,简直找到宝藏了,去他的官方文档。

qI3qqai.png!web

我在里面找到了长度限制的demo,很简单,min和max两个标签就搞定了。跑一下完全没有问题。

type Login struct {
	User     string `form:"user" binding:"required"`
	Password string `form:"password" binding:"required,min=6,max=12"`
}
curl -d "user=pingye&password=12345" http://localhost:8080/login 
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'min' tag"}

curl -d "user=pingye&password=1234567890123" http://localhost:8080/login
{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'max' tag"}

实现了两个校验了,还剩下用户名的字母和数字限制,让我震惊的是,我以为随便说的这个限制要用多种组合来实现,竟然轻松就找到了一个对应的,简直太棒了(强烈推荐这个库,看来后面有必要出一期这个库的介绍),很简单,加上一个名叫 alphanum 的规则就可以实现了。

type Login struct {
	User     string `form:"user" binding:"required,alphanum"`
	Password string `form:"password" binding:"required,min=6,max=12"`
}

今天我的任务结束了,各位是不是需要查看源码,来吧来吧, 点击查看源码 ,顺便STAR一下我哈。

Go语言库示例开源项目「 golang-examples 」欢迎 star~

https://github.com/pingyeaa/golang-examples

感谢大家的观看,如果觉得文章对你有所帮助,欢迎关注公众号「平也」,聚焦Go语言与技术原理。

22mEZru.png!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK