天天看點

深入Gin架構内幕(二) 頂

在上一篇文章深入gin架構内幕(一)中,主要介紹了Gin架構中是如何建立一個HTTP服務以及内部的核心結構和常用的一些結構體方法,并在最後以一個簡單的示例來詳細講解Gin架構内部具體是如何運作的,但是在最後我們會發現使用了一個

Context

引用對象的一些方法來傳回具體的HTTP響應資料,在本篇文章中,我們将繼續學習和分析Gin架構内幕。

在開始分析之前,我們先簡單回顧一下上一個章節中講到的Gin架構中的幾個核心的結構.

Gin架構中的幾個核心結構

Gin架構中的幾個重要的模型:

  • Engine

    : 用來初始化一個

    gin

    對象執行個體,在該對象執行個體中主要包含了一些架構的基礎功能,比如日志,中間件設定,路由控制(組),以及handlercontext等相關方法.源碼檔案
  • Router

    : 用來定義各種路由規則和條件,并通過HTTP服務将具體的路由注冊到一個由context實作的handler中
  • Context:

    Context

    是架構中非常重要的一點,它允許我們在中間件間共享變量,管理整個流程,驗證請求的json以及提供一個json的響應體. 通常情況下我們的業務邏輯處理也是在整個Context引用對象中進行實作的.
  • Bind: 在Context中我們已經可以擷取到請求的詳細資訊,比如HTTP請求頭和請求體,但是我們需要根據不同的HTTP協定參數來擷取相應的格式化 資料來處理底層的業務邏輯,就需要使用

    Bind

    相關的結構方法來解析context中的HTTP資料

1.Gin架構對HTTP響應資料的處理

我們在深入Gin架構内幕(一)中,以一個簡單的Gin執行個體來具體講解它内部是如何建立一個Http服務,并且注冊一個路由來接收使用者的請求,在示例程式中我們使用了

Context

引用對象的

String

方法來處理HTTP服務的資料響應,是以在整個Gin架構中緊跟

Router

模型結構的就要屬

Context

結構了,該結構體主要用來處理整個HTTP請求的上下文資料,也是我們在開發HTTP服務中相對比較重要的一個結構體了。

# 深入Gin架構内幕(一)中的示例
$ cat case1.go
package main
import (
    "net/http"

    "github.com/gin-gonic/gin"
)
func main() {
    ginObj := gin.Default()
    ginObj.Any("/hello",func(c *gin.Context){
        c.String(http.StatusOK,"Hello BGBiao.")
    })
    ginObj.Run("localhost:8080")
}           

複制

我們可以看到,在使用Gin架構後,我們隻需要很簡單的代碼,即可以快速運作一個傳回

Hello BGBiao.

的HTTP服務,而在

ginObj.Any

方法中,我們傳入了一個參數為

Context

引用類型的匿名函數,并在該函數内部采用

String(code,data)

方法來處理HTTP服務的響應資料(傳回Hello BGBiao字元串),這個時候,你可能會想,我們在企業内部都是前後端分離,通常情況下後端僅會提供

RESTful API

,并通過

JSON

格式的資料和前端進行互動,那麼Gin是如何處理其他非字元串類型的資料響應呢,這也是我們接下來要主要講的

Context

結構模型。

2.Gin架構中的Context結構體

注意:

在Gin架構中由

Router

結構體來負責路由和方法(URL和HTTP方法)的綁定,内的Handler采用

Context

結構體來處理具體的HTTP資料傳輸方式,比如HTTP頭部,請求體參數,狀态碼以及響應體和其他的一些常見HTTP行為。

Context結構體

:

type Context struct {
    // 一個包含size,status和ResponseWriter的結構體
    writermem responseWriter
    // http的請求體(指向原生的http.Request指針)
    Request   *http.Request
    // ResonseWriter接口
    Writer    ResponseWriter

    // 請求參數[]{"Key":"Value"}
    Params   Params
    handlers HandlersChain
    index int8
    // http請求的全路徑位址
    fullPath string
    // gin架構的Engine結構體指針
    engine   *Engine
    // 每個請求的context中的唯一鍵值對
    Keys map[string]interface{}
    // 綁定到所有使用該context的handler/middlewares的錯誤清單
    Errors errorMsgs
    // 定義了允許的格式被用于内容協商(content)
    Accepted []string
    // queryCache 使用url.ParseQuery來緩存參數查詢結果(c.Request.URL.Query())
    queryCache url.Values
    // formCache 使用url.ParseQuery來緩存PostForm包含的表單資料(來自POST,PATCH,PUT請求體參數)
    formCache url.Values
}           

複制

Context結構體常用的一些方法

基本方法

:

  • Copy(): 傳回目前正在使用的context的拷貝(context指針),當這個context必須在goroutine中用時,該方法比較有用
  • HandlerName(): 傳回目前主handler的名稱(比如:handler為handleGetUsers(),該方法将傳回"main.handleGetUsers")
  • HandlerNames(): 傳回所有注冊的handler的名稱
  • Handler()

    : 傳回目前的主handler(

    func (c *Context) Handler() HandlerFunc

    )
  • FullPath(): 傳回一個比對路由的全路徑(uri: "/user/:id",c.FullPath() == "/user/:id" )

http常用方法

:

  • ClientIP() string: 傳回用戶端ip(該方法會解析

    X-Real-IP

    ,

    X-Forwarded-For

    )
  • ContentType() string: 傳回HTTP的Content-Type頭
  • IsWebsocket() bool: 傳回是否為ws連結

流控相關的方法:

  • Next(): 該方法僅被使用在middleware中,它會在被調用的handler鍊内部執行pending handler
  • IsAborted(): 如果目前的context被終止了,該方法傳回true
  • Abort(): 該函數可以從正在被調用中保護pending handler. 該方法停止後不會停止目前正在執行的handler. 比如我們有一個鑒權的中間件來驗證請求是否有權限,如果認證失敗了(使用者資訊異常等),此時調用Abort()來確定後面的handler不再被調用
  • AbortWithStatus(code int): 同上,在會寫入狀态碼。context.AbortWithStatus(401)即可表示上述的鑒權失敗
  • AbortWithStatusJSON(code int, jsonObj interface{}): 同上,會再加響應資料.該方法會停止整個handler鍊,再寫入狀态碼和json的響應體,同時也會設定Content-Type="application/json"
  • AbortWithError(code int, err error) *Error: 同上傳回錯誤資訊

錯誤管理

:

  • Error(err error) *Error: 傳回一些錯誤對象

中繼資料管理

:

  • Set(key string, value interface{}): 給目前這個context設定一個新的鍵值對
  • Get(key string) (value interface{}, exists bool): 傳回指定的key的值,以及是否存在
  • MustGet(key string) interface{}: 傳回指定key的值,不存在則panic
  • GetString(key string) (s string): 以string類型傳回指定的key
  • GetBool(key string) (b bool): 傳回配置設定給該key的值(bool類型)
  • GetInt(key string) (i int):
  • GetStringSlice(key string) (ss []string): 傳回key的slice類型
  • GetStringMap(key string) (sm map[string]interface{}): 傳回interface{}類型的map結構
  • GetStringMapString(key string) (sms map[string]string): 傳回string類型的map結構
  • GetStringMapStringSlice(key string) (smss map[string][]string): 同理

輸入資料

:

  • Param(key string) string: 傳回URL的參數值(uri_patten: "/user/:id",url: "/user/john",c.Param("id") = "john")
  • Query(key string) string: 傳回url中的查詢參數值(url: "/path?id=1234&name=Manu&value=",c.Query("id")為1234,c.Query("name")為Manu,c.Query("value")為空)
  • DefaultQuery(key, defaultValue string) string: 傳回url中的查詢參數的預設值(同上,但是c.Query("value")就沒有值,該方法可以設定預設值)
  • GetQuery(key string) (string, bool): 同Query()方法,并且會傳回狀态,如果對應的key不存在,傳回("",false)
  • QueryArray(key string) []string: 傳回指定key的對應的array(slice的長度取決于給定key的參數的數量)
  • GetQueryArray(key string) ([]string, bool): 同上,會傳回狀态
  • QueryMap(key string) map[string]string: 傳回指定key對應map類型
  • GetQueryMap(key string) (map[string]string, bool): 同上,并且會傳回狀态
  • PostForm(key string) string: 該方法傳回一個從POST 請求的urlencode表單或者multipart表單資料,不存在時傳回空字元串
  • DefaultPostForm(key, defaultValue string) string: 同上,key不存在時傳回預設值
  • GetPostForm(key string) (string, bool): 同PostForm()方法,并且會傳回狀态
  • PostFormArray(key string) []string: 該方法傳回指定key的字元串類型的slice
  • GetPostFormArray(key string) ([]string, bool): 同上,并傳回狀态
  • PostFormMap(key string) map[string]string: 傳回指定key的map類型
  • GetPostFormMap(key string) (map[string]string, bool): 同上,并傳回狀态
  • FormFile(name string) (*multipart.FileHeader, error): 傳回指定key的第一個檔案(用作檔案上傳)
  • MultipartForm() (*multipart.Form, error): 該方法解析multipart表單,包含file檔案上傳
  • SaveUploadedFile(file *multipart.FileHeader, dst string) error: 該方法用來上傳指定的檔案頭到目标路徑(dst)

Bind家族相關方法

:

  • Bind(obj interface{}) error: 自動解析

    Content-Type

    并綁定到指定的binding引擎
  • BindJSON(obj interface{}) error: 同上,binding引擎為

    binding.JSON

  • BindXML(obj interface{}) error:
  • BindQuery(obj interface{}) error:
  • BindYAML(obj interface{}) error:
  • BindHeader(obj interface{}) error:
  • BindUri(obj interface{}) error: 使用

    binding.Uri

    來綁定傳遞的結構體指針
  • MustBindWith(obj interface{}, b binding.Binding) error: 使用指定的binding引擎來綁定傳遞的結構體指針(當有任何錯誤時,終止請求并傳回400)

ShouldBind家族相關方法

:

  • ShouldBind(obj interface{}) error: 同上述的Bind()方法,但是該方法在json結構無效時不會傳回400
  • ShouldBindJSON(obj interface{}) error:
  • ShouldBindXML(obj interface{}) error:
  • ShouldBindQuery(obj interface{}) error:
  • ShouldBindYAML(obj interface{}) error:
  • ShouldBindHeader(obj interface{}) error:
  • ShouldBindUri(obj interface{}) error:
  • ShouldBindWith(obj interface{}, b binding.Binding) error: 等同于MustBindWith()方法
  • ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error): 和ShouldBindWith()方法相似,但是他會存儲請求體到context中,當下次調用時可以重用(因為該方法是在binding之前讀取body,是以在你隻使用一次時,為了更好的性能還是使用ShouldBindWith會比較好)

HTTP響應相關的方法

:

  • Status(code int): 設定http的響應碼
  • Header(key, value string): 是

    c.Writer.Header().Set(key, value)

    的簡單實作,在響應體重寫入一個header,如果value為空,則相當于調用了

    c.Writer.Header().Del(key)

  • GetHeader(key string) string: 傳回請求體重的header
  • GetRawData() ([]byte, error): 傳回流式資料
  • SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool): 該方法将設定一個Set-Cookie到ResponseWriter的頭中(注意:name必須是一個合法可用的名稱,無效的coookie可能會被丢棄)
  • Cookie(name string) (string, error): 傳回名稱為name的cookie
  • Render(code int, r render.Render): 該方法寫入響應頭并調用render.Render去渲染資料
  • HTML(code int, name string, obj interface{}): 該方法使用指定檔案模闆名稱去渲染http模闆(同時會更新狀态碼并設定Content-Type as "text/html".)
  • IndentedJSON(code int, obj interface{}): 該方法會序列化對象obj為一個pretty JSON 資料到響應體中,同時設定Content-Type as "application/json"(pretty JSON需要消耗cpu和帶寬,強烈建議生産使用

    Context.JSON()

    )
  • SecureJSON(code int, obj interface{}): 同上,會序列化成

    Secure Json

  • JSONP(code int, obj interface{}):
  • JSON(code int, obj interface{}): 序列化為JSON,并寫Content-Type:"application/json"頭
  • AsciiJSON(code int, obj interface{}):
  • PureJSON(code int, obj interface{}):
  • XML(code int, obj interface{}): 序列化成

    xml

    格式,并寫Content-Type:"application/xml"
  • YAML(code int, obj interface{}): 序列化成

    yaml

  • ProtoBuf(code int, obj interface{}): 序列化成

    probuf

  • String(code int, format string, values ...interface{}): 将制定的string寫入響應體
  • Redirect(code int, location string): 重定向
  • Data(code int, contentType string, data []byte): 寫一些資料到響應體重,并更新響應碼
  • DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string): 寫一些制定模闆的資料到響應體中,并更新狀态碼
  • File(filepath string): 以一種高效方式将制定檔案寫入響應體資料中
  • FileAttachment(filepath, filename string): 同上,但是在用戶端檔案會被直接下載下傳下來
  • SSEvent(name string, message interface{}): 寫Server-Sent Event到響應資料中
  • Stream(step func(w io.Writer) bool) bool: 發送一個流式的響應資料并傳回狀态

3.Gin執行個體示例

3.1傳回json格式的資料

為了解決我們在開頭提到的問題,我們将使用context引用對象的JSON家族方法來處理該需求

# 使用context來傳回json格式的資料
$ cat case2.go
package main

import (
    "github.com/gin-gonic/gin"
)

// 我們定義一個通用的格式化的響應資料
// 在Data字段中采用空接口類型來實際存放我們的業務資料
type restData struct {
    Data        interface{}   `json:"data"`
    Message     string        `json:"message"`
    Status      bool          `json:"status"`
}

func main() {
    // mock一個http響應資料
    restdata := &restData{"Hello,BGBiao","",true}
    restdata1 := &restData{map[string]string{"name":"BGBiao","website":"https://bgbiao.top"},"",true}

    // 使用Gin架構啟動一個http接口服務
    ginObj := gin.Default()
    ginObj.GET("/api/test",func(c *gin.Context){
       // 我們的handlerFunc中入參是一個Context結構的引用對象c
       // 是以我們可以使用Context中的JSON方法來傳回一個json結構的資料
       // 可用的方法有如下幾種,我們可以根據實際需求進行選擇
       /*
          IndentedJSON(code int, obj interface{}): 帶縮進的json(消耗cpu和mem)
          SecureJSON(code int, obj interface{}): 安全化json
          JSONP(code int, obj interface{})
          JSON(code int, obj interface{}): 序列化為JSON,并寫Content-Type:"application/json"頭
       */
       c.JSON(200,restdata)
    })
    ginObj.GET("/api/test1",func(c *gin.Context){
        c.IndentedJSON(200,restdata1)
    })

    ginObj.Run("localhost:8080")
}


# 執行個體運作(這裡成功将我們寫的兩個api接口進行對外暴露)
$ go run case2.go
....
....
[GIN-debug] GET    /api/test                 --> main.main.func1 (3 handlers)
[GIN-debug] GET    /api/test1                --> main.main.func2 (3 handlers)

# 接口測試通路
$ curl localhost:8080/api/test
{"data":"Hello,BGBiao","message":"","status":true}
$ curl localhost:8080/api/test1
{
    "data": {
        "name": "BGBiao",
        "website": "https://bgbiao.top"
    },
    "message": "",
    "status": true
}%           

複制

當然上面我們僅以JSON格式來示例,類似的方式我們可以使用

XML

,

YAML

,

ProtoBuf

等方法來輸出指定格式化後的資料。

3.2其他常用的基本方法

注意:

在其他基本方法中我們仍然使用上述示例代碼中的主邏輯,主要用來測試基本的方法.

# 我們在/api/test這個路由中增加如下兩行代碼
// 設定響應體中的自定義header(通常我們可以通過自定義頭來實作一個内部辨別)
c.Header("Api-Author","BGBiao")
// GetHeader方法用來擷取指定的請求頭,比如我們經常會使用請求中的token來進行接口的認證和鑒權
// 這裡由于我們使用的restdata的指針,通過GetHeader方法擷取到token指派給Message
// ClientIP()方法用于擷取用戶端的ip位址
restdata.Message = fmt.Sprintf("token:%s 目前有效,用戶端ip:%s",c.GetHeader("token"),c.ClientIP())

# 通路接口示例(我們可以看到在響應體中多了一個我們自定義的Api-Author頭,并且我們将請求頭token的值)
$ curl -H 'token:xxxxxxxx' localhost:8080/api/test -i
HTTP/1.1 200 OK
Api-Author: BGBiao
Content-Type: application/json; charset=utf-8
Date: Sun, 12 Jan 2020 14:41:01 GMT
Content-Length: 66

{"data":"Hello,BGBiao","message":"token:xxxxxxxx 目前有效,用戶端ip:127.0.0.1","status":true}           

複制

3.3使用者資料輸入

當然到這裡後,你可能還會有新的疑問,就是通常情況下,我們開發後端接口會提供一些具體的參數,通過一些具體資料送出來實作具體的業務邏輯處理,這些參數通常會分為如下三類:

  • 使用HTTP GET方法擷取到的url中的一些查詢參數來執行更具體的業務邏輯(比如我們查詢資料的指定條數之類的)
  • 使用HTTP POST GET等其他方式以form表單方式送出的資料來驗證和處理使用者資料
  • 在URL中擷取一些可變參數(比如通常我們的url會定義為"/api/uid/:id"來表示使用者id相關的接口,這個時候通常需要擷取到url中的id字段)

以上的基本需求,幾乎都可以在Context結構體的

輸入資料

中找到響應的方法.

# 接下來,我們依然在上述的代碼中進行修改,增加如下路由
$ cat case2.go
....
....
    // 比如我們該接口時用來擷取全部資料,但是我們希望在url中增加參數來限制資料條數
    datas := []string{"Golang","Python","Docker","Kubernetes","CloudNative","DevOps"}
    ginObj.GET("/api/testdata",func(c *gin.Context){
        limit := c.Query("limit")
        // 其實既然這裡我們已經确定需求了,當使用者沒有輸入limit參數時我們就可以設定預設值
        // DefaultQuery("limit","1")
        // 同時我們其實也可以使用GetQuery方法來擷取參數解析狀态,即是否有對應的參數
        // 還有QueryArray和GetQueryArray類似的方法
        if limit != "" {
            num,_ := strconv.Atoi(limit)
            restdata1.Data = datas[:num]
        }else {
            restdata1.Data = datas
        }
        c.IndentedJSON(200,restdata1)

    })

    // 使用form表單方式送出資料
    ginObj.POST("/api/testdata",func(c *gin.Context){
        // 使用c.PostForm方法來送出一個data資料
        // 同時我們可以使用DefaultPostForm方法來給送出資料一個預設值,比如我們有些參數是希望有預設值的
        // 當然也可以使用GetPostForm,PostFormArray,PostFormArray方法來擷取多個資料和狀态
        // data := c.PostForm("data")
        // datas = append(datas,data)
        /* 這裡可能會有個問題就是同時送出多個資料時,使用PostForm方法就會不那麼好使了
        通常情況下回使用PostFormArray方法
        */
        data := c.PostFormArray("data")
        datas = append(datas,data...)
        restdata1.Data = datas
        c.IndentedJSON(200,restdata1)
    })

    // 擷取url中的路徑參數
    ginObj.GET("/api/testdata/:data",func(c *gin.Context){
        data := c.Param("data")
        for _,rawData := range datas {
            if data == rawData {
                restdata1.Data = data
                break
            }
        }
        if restdata1.Data != data {
            restdata1.Data = ""
            restdata1.Message = fmt.Sprintf("%v 不存在",data)
            restdata1.Status = false
        }
        c.IndentedJSON(200,restdata1)
    })
....
....



# 請求示例接口
# 我們可以看到使用GET方法預設會擷取到全部資料,但是如果有了limit參數後,我們就可以限制資料的條數
$ curl -H 'token:xxxxxxxx' localhost:8080/api/testdata
{
    "data": [
        "Golang",
        "Python",
        "Docker",
        "Kubernetes",
        "CloudNative",
        "DevOps"
    ],
    "message": "",
    "status": true
}%
$ curl -H 'token:xxxxxxxx' "localhost:8080/api/testdata?limit=2"
{
    "data": [
        "Golang",
        "Python"
    ],
    "message": "",
    "status": true
}%

# 當我們使用post接口往服務送出資料時,就可以讓服務端按照需求進行資料處理
curl -X POST  -d data="vue" "localhost:8080/api/testdata"
{
    "data": [
        "Golang",
        "Python",
        "Docker",
        "Kubernetes",
        "CloudNative",
        "DevOps",
        "vue"
    ],
    "message": "",
    "status": true
}%

# 當我們同時需要送出多份資料時,可以使用PostFormArray方法,同時送出多份資料(可以了解為批量送出)
$ curl -X POST -d data="vue" -d data="Rust" "localhost:8080/api/testdata"

# 擷取URL中的參數值
$ curl "localhost:8080/api/testdata/Golang"
{
    "data": "Golang",
    "message": "",
    "status": true
}%

$ curl "localhost:8080/api/testdata/Java"
{
    "data": "",
    "message": "Java 不存在",
    "status": false
}%           

複制