Gin框架系列04:趣談參數(shù)綁定與校驗(yàn)
導(dǎo)讀
在第二節(jié),我們學(xué)習(xí)了Gin框架的路由定義與參數(shù)接收,今天應(yīng)一位同學(xué)的要求,來講解一下參數(shù)的綁定與校驗(yàn)。
為什么校驗(yàn)參數(shù)?
本不必拋出這個問題的,但顧及到初出茅廬的同學(xué),這里解釋一下。

假設(shè)做一個注冊接口,傳過來的用戶名是不是不能太騷氣?比如一堆空格和符號之類的;密碼是不是不能太長也不能太短?手機(jī)號是不是要符合規(guī)則?性別是不是不能填人妖?

另外,登錄的時候我們也需要驗(yàn)證賬號密碼是不是正確的,那么為了方便上手,咱就先來個簡單示例,做登錄驗(yàn)證。
激情演示
做登錄之前得先想清楚需要對用戶名密碼做什么樣的限制,比如他們都不能為空、用戶名只能是字母或數(shù)字、密碼長度只能在6位到12位之間等,如果各位看官沒有異議,接下來我就拿上述的這幾個條件來演示了。

定義結(jié)構(gòu)體與接口
首先得有個地方存接收到的用戶名、密碼參數(shù),那就定一個名叫Login的結(jié)構(gòu)體吧。
type Login struct {
User string
Password string
}
然后再定義一個登錄接口,這塊不知道啥意思的同學(xué)可以去看我的第二篇教程。
router.POST("/login", func(c *gin.Context) {
})
接口是定了,怎么才能把接收的參數(shù)放到Login結(jié)構(gòu)體里去呢?

綁定參數(shù)
我們?nèi)シ幌?code>gin.Context下面都有些什么好東西可以拿來用。看到了一個叫做Bind的東東,官方說它可以自動根據(jù)Content-Type設(shè)置的值解析請求過來的參數(shù),然后把參數(shù)設(shè)置到結(jié)構(gòu)體里。
func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b)
}
哇塞,這就有意思了,調(diào)用一下試試。記得把Login的引用傳過去,畢竟人家還要賦值的。
router.POST("/login", func(c *gin.Context) {
var login Login
c.Bind(&login)
c.JSON(200, login)
})
果不其然,跑起來的同學(xué)應(yīng)該可以發(fā)現(xiàn),它失敗了,并沒有取到我想要的參數(shù)值。
curl -d "user=pingye&password=123" http://localhost:8080/login
{"User":"","Password":""}
這不對啊,不是說好做彼此的天使嗎?

不行,我要一層一層剝開gin框架是怎么處理的。

綁定失敗,剖析源碼
經(jīng)過長達(dá)60分鐘的精心研究,我終于發(fā)現(xiàn)了終極奧義,先把我繪制的圖貼上。

事情是這樣的,我們調(diào)用的Bind方法實(shí)際調(diào)用了兩個方法binding.Default和c.MustBindWith,前者的主要作用是根據(jù)終端請求的Content-Type選擇處理器,沒辦法,gin太強(qiáng),支持的類型太多了。我們剛才的請求方式被理所應(yīng)當(dāng)?shù)姆峙浣o了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方法,它的作用就是調(diào)用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方法主要就干了兩件事情,第一是解析傳過來的表單參數(shù),第二是找到結(jié)構(gòu)體里的tagform進(jìn)行匹配賦值。看到這里我就明白了,原來只需要在Login后面加上一個tag。
繼續(xù)綁定參數(shù)
那我們加上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"}
看到了這里,聰明的你應(yīng)該涌出了很多想法,剛才說支持那么多類型,前端傳的是json咋搞呢?同學(xué)們可以自己試一下,現(xiàn)有的這套代碼啥都不用改就可以解析json,因?yàn)閖sonBinding并沒有去Login結(jié)構(gòu)體找tag,所以不用在后面加上json:"user"的標(biāo)識。
curl -H "Content-Type:application/json" -d '{"user":"pingye","password":"123455"}' http://localhost:8080/login
{"User":"pingye","Password":"123455"}
至于其他的類型,同學(xué)們可以自己去動手試驗(yàn)一下,我們必須得到參數(shù)驗(yàn)證環(huán)節(jié)了。
參數(shù)驗(yàn)證
OK,這就到了激動人心的參數(shù)驗(yàn)證時刻了,再回顧一下剛才的需求,用戶名和密碼不能為空,用戶名只能是英文和數(shù)字,密碼長度必須得在6到12位。
gin官方給出的示例是直接在tag中加校驗(yàn)規(guī)則,比如不能為空,就加上binding:"required"。
type Login struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
}
在驗(yàn)證的地方也做一下處理,Bind會自動幫我們進(jìn)行校驗(yàn),如果校驗(yàn)失敗會返回一個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)
})
放心,參數(shù)驗(yàn)證的演示不會就這么結(jié)束的。

鑒于官方文檔給的信息太少了,我們還是通過源碼去找更多線索吧,通過源代碼可以看到,gin的參數(shù)驗(yàn)證實(shí)際上并不是自己實(shí)現(xiàn)的,而是使用了一個叫做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的文件,有非常多的示例與解釋,簡直找到寶藏了,去他的官方文檔。

我在里面找到了長度限制的demo,很簡單,min和max兩個標(biāo)簽就搞定了。跑一下完全沒有問題。
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"}
實(shí)現(xiàn)了兩個校驗(yàn)了,還剩下用戶名的字母和數(shù)字限制,讓我震驚的是,我以為隨便說的這個限制要用多種組合來實(shí)現(xiàn),竟然輕松就找到了一個對應(yīng)的,簡直太棒了(強(qiáng)烈推薦這個庫,看來后面有必要出一期這個庫的介紹),很簡單,加上一個名叫alphanum的規(guī)則就可以實(shí)現(xiàn)了。
type Login struct {
User string `form:"user" binding:"required,alphanum"`
Password string `form:"password" binding:"required,min=6,max=12"`
}
今天我的任務(wù)結(jié)束了,各位是不是需要查看源碼,來吧來吧,點(diǎn)擊查看源碼,順便STAR一下我哈。
Go語言庫示例開源項(xiàng)目「golang-examples」歡迎star~
https://github.com/pingyeaa/golang-examples
感謝大家的觀看,如果覺得文章對你有所幫助,歡迎關(guān)注公眾號「平也」,聚焦Go語言與技術(shù)原理。


浙公網(wǎng)安備 33010602011771號