在上一篇文章深入gin架構内幕(一)中,主要介紹了Gin架構中是如何建立一個HTTP服務以及内部的核心結構和常用的一些結構體方法,并在最後以一個簡單的示例來詳細講解Gin架構内部具體是如何運作的,但是在最後我們會發現使用了一個 Context
引用對象的一些方法來傳回具體的HTTP響應資料,在本篇文章中,我們将繼續學習和分析Gin架構内幕。
在開始分析之前,我們先簡單回顧一下上一個章節中講到的Gin架構中的幾個核心的結構.
Gin架構中的幾個核心結構
Gin架構中的幾個重要的模型:
-
: 用來初始化一個Engine
對象執行個體,在該對象執行個體中主要包含了一些架構的基礎功能,比如日志,中間件設定,路由控制(組),以及handlercontext等相關方法.源碼檔案gin
-
: 用來定義各種路由規則和條件,并通過HTTP服務将具體的路由注冊到一個由context實作的handler中Router
- Context:
是架構中非常重要的一點,它允許我們在中間件間共享變量,管理整個流程,驗證請求的json以及提供一個json的響應體. 通常情況下我們的業務邏輯處理也是在整個Context引用對象中進行實作的.Context
- Bind: 在Context中我們已經可以擷取到請求的詳細資訊,比如HTTP請求頭和請求體,但是我們需要根據不同的HTTP協定參數來擷取相應的格式化 資料來處理底層的業務邏輯,就需要使用
相關的結構方法來解析context中的HTTP資料Bind
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: 自動解析
并綁定到指定的binding引擎Content-Type
- 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): 是
的簡單實作,在響應體重寫入一個header,如果value為空,則相當于調用了c.Writer.Header().Set(key, 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{}): 序列化成
格式,并寫Content-Type:"application/xml"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
}%
複制