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 上,這樣做其實是不建議的,不過大家都這麼做,只能順其自然了。這麼做的缺點很明顯:

這裏就不具體羅列了,反正缺點挺多的。

這種參數也有兩種獲取方式:

方式 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