Gin框架系列04:趣談參數綁定與校驗
- 2020 年 4 月 10 日
- 筆記
導讀
在第二節,我們學習了Gin框架的路由定義與參數接收,今天應一位同學的要求,來講解一下參數的綁定與校驗。
為什麼校驗參數?
本不必拋出這個問題的,但顧及到初出茅廬的同學,這裡解釋一下。

假設做一個註冊介面,傳過來的用戶名是不是不能太騷氣?比如一堆空格和符號之類的;密碼是不是不能太長也不能太短?手機號是不是要符合規則?性別是不是不能填人妖?

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

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

綁定參數
我們去翻一下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":""} 這不對啊,不是說好做彼此的天使嗎?

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

綁定失敗,剖析源碼
經過長達60分鐘的精心研究,我終於發現了終極奧義,先把我繪製的圖貼上。

事情是這樣的,我們調用的Bind方法實際調用了兩個方法binding.Default和c.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方法主要就幹了兩件事情,第一是解析傳過來的表單參數,第二是找到結構體里的tagform進行匹配賦值。看到這裡我就明白了,原來只需要在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) }) 放心,參數驗證的演示不會就這麼結束的。

鑒於官方文檔給的資訊太少了,我們還是通過源碼去找更多線索吧,通過源程式碼可以看到,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的文件,有非常多的示例與解釋,簡直找到寶藏了,去他的官方文檔。

我在裡面找到了長度限制的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語言與技術原理。


