gin 源碼閱讀 -4- - 友好的請求參數處理
hi,大家好,我是 haohongfan。
通過 gin 的路由,已經把請求分配到具體的函數里面裏面了,下面就要開始處理具體的業務邏輯了。
這裏就進入 gin 封裝的非常重要的的功能,對請求參數快速解析,讓我們不糾結於參數的繁瑣處理。當然這是對於比較標準的參數處理纔可以,對於那些自定義的參數格式只能自己處理了。
參數風格
對於 RESTful 風格的 http 請求來說,參數的表現會有下面幾種方式:
URI 參數
什麼是 URI 參數?RESTful 風格的請求,某些請求的參數會通過 URI 來表現。
舉個簡單的例子:張三通過網上銀行給李四轉了 500 元,這個路由可以這麼設計:
xxx.com/:name/transfer/:money/to/:name
非常具體的體現:
xxx.com/zhangsan/transfer/500/to/lisi
當然你會說這個路由設計會比較醜陋,不過在 URI 裏面增加參數有的時候是比較方便的,gin 支持這種方式獲取參數。
// This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", uriFunc)
對於獲取這種路由參數,gin 提供了兩種方式去解析這種參數。
方式 1:Param
func uriFunc(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
}
方式 2:bindUri
type Person struct {
Name string `uri:"name" binding:"required"`
}
func uriFunc(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err.Error()})
return
}
c.JSON(200, gin.H{"name": person.Name)
}
其實現原理很簡單,就是在創建路由樹的時候,將路由參數以及對應的值放入一個特定的 map 中即可。
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
}
QueryString Parameter
query String 即路由的 ?
之後的所帶的參數,這種方式是比較常見的。
例如:/welcome?firstname=Jane&lastname=Doe
這裏要注意的是,不管是 GET 還是 POST 都可以帶 queryString Parameter。我曾經遇到某公司所有的參數都掛在 query string 上,這樣做其實是不建議的,不過大家都這麼做,只能順其自然了。這麼做的缺點很明顯:
-
容易突破 URI 的長度限制,導致接口參數被截斷。一般情況下服務器爲了安全會對 URL 做長度限制,最大爲 2048
-
同時服務器也會對傳輸的大小也是有限制的,一般是 2k
-
當然這麼做也是不安全的,都是明文的
這裏就不具體羅列了,反正缺點挺多的。
這種參數也有兩種獲取方式:
方式 1:Query
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")
方式 2:Bind
type Person struct {
FirstName string `form:"name"`
}
func queryFunc(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println(person.Name)
}
}
實現原理:其實很簡單就是將請求參數解析出來而已,利用的 net/url 的相關函數。
//net/url.go:L1109
func (u *URL) Query() Values {
v, _ := ParseQuery(u.RawQuery)
return v
}
Form
Form 一般還是更多用在跟前端的混合開發的情況下。Form 可以用於所有的方法 POST,GET,HEAD,PATCH ……
這種參數也有兩種獲取方式:
方式 1:
name := c.PostForm("name")
方式 2:
type Person struct {
Name string `form:"name"`
}
func formFunc(c *gin.Context) {
var person Person
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
}
}
Json Body
Json Body 是被使用最多的方式,基本上各種語言庫對 json 格式的解析非常完善了,而且還在不斷的推陳出新。
gin 對 json 的解析只有一種方式。
type Person struct {
Name string `json:"name"`
}
func jsonFunc(c *gin.Context) {
var person Person
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
}
}
gin 默認是使用的 go 內置的 encoding/json 庫,內置的 json 在 go 1.12 後性能得到了很大的提高。不過 Go 對接 PHP 的接口,如果用內置的 json 庫簡直就是一種折磨,gin 可以使用 jsoniter 來代替,只需要在編譯的時候加上標誌即可:"go build -tags=jsoniter .",強烈建議對接 PHP 接口的同學,嘗試 jsoniter 這個庫,讓你不再受 PHP 接口參數類型不確定之苦。
當然 gin 還支持其他類型參數的解析,如 Header,XML,YAML,Msgpack,Protobuf 等,這裏就不再具體介紹了。
Bind 系列函數的源碼剖析
使用 gin 解析 request 的參數,按照我的實踐來看,使用 Bind 系列函數還是比較好一點,因爲這樣請求的參數會比較好歸檔、分類,也有助於後續的接口升級,而不是將接口的請求參數分散不同的 handler 裏面。
初始化 binding 相關對象
gin 在程序啓動就會默認初始化好 binding 相關的變量
// binding:L74
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
)
ShoudBind 與 MustBind 的區別
bind 相關的系列函數大體上分爲兩類 ShoudBind 和 MustBind。實現上基本一樣,爲了有區別的 MustBind 在解析失敗的時候,返回 HTTP 400 狀態。
MustBindWith:
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // nolint: errcheck
return err
}
return nil
}
ShoudBindWith:
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
匹配對應的參數 decoder
不管是 MustBind 還是 ShouldBind,總體上解析又可以分爲兩類:一種是讓 gin 自己判斷使用哪種 decoder,另外一種就是指定某種 decoder。自己判斷使用哪種 decoder 比 指定 decoder 多了一步判斷,其他的都是一樣的。
func (c *Context) ShouldBind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}
func Default(method, contentType string) Binding {
if method == http.MethodGet {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
case MIMEYAML:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
default: // case MIMEPOSTForm:
return Form
}
}
ShouldBind/MustBind 會根據傳入的 ContentType 來判斷該使用哪種 decoder。不過對於 Header 和 Uri 方式的參數,只能用指定方式的 decoder 了。
總結
本篇文章主要介紹了 gin 是如何快速處理客戶端傳遞過的參數的。寫文章不易請大家幫忙點擊 在看,點贊,分享。
當然如果想和我做朋友,圍觀朋友圈,可以加我的個人微信(微信號:forever_hhf)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/2OI9vh_LGPxtGkhF-AnWDA