Gin框架系列04:趣談參數綁定與校驗

  • 2020 年 4 月 10 日
  • 筆記

導讀

在第二節,我們學習了Gin框架的路由定義與參數接收,今天應一位同學的要求,來講解一下參數的綁定與校驗。

為什麼校驗參數?

本不必拋出這個問題的,但顧及到初出茅廬的同學,這裡解釋一下。

file

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

file

另外,登錄的時候我們也需要驗證帳號密碼是不是正確的,那麼為了方便上手,咱就先來個簡單示例,做登錄驗證。

激情演示

做登錄之前得先想清楚需要對用戶名密碼做什麼樣的限制,比如他們都不能為空、用戶名只能是字母或數字、密碼長度只能在6位到12位之間等,如果各位看官沒有異議,接下來我就拿上述的這幾個條件來演示了。

file

定義結構體與介面

首先得有個地方存接收到的用戶名、密碼參數,那就定一個名叫Login的結構體吧。

type Login struct {  	User     string  	Password string  }  

然後再定義一個登錄介面,這塊不知道啥意思的同學可以去看我的第二篇教程。

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

介面是定了,怎麼才能把接收的參數放到Login結構體里去呢?

file

綁定參數

我們去翻一下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":""}  

這不對啊,不是說好做彼此的天使嗎?

file

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

file

綁定失敗,剖析源碼

經過長達60分鐘的精心研究,我終於發現了終極奧義,先把我繪製的圖貼上。

file

事情是這樣的,我們調用的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)  })  

放心,參數驗證的演示不會就這麼結束的。

file

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

file

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

Tags: