gin是怎么傳參數


gin 源碼閱讀系列文章列表:

gin 源碼閱讀(1) - gin 與 net/http 的關系gin 源碼閱讀(2) - http請求是如何流入gin的?gin 源碼閱讀(3) - gin 路由的實現剖析

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 是如何快速處理客戶端傳遞過的參數的。寫文章不易請大家幫忙點擊 在看,點贊,分享


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM