GO語言web框架Gin之完全指南


作為一款企業級生產力的web框架,gin的優勢是顯而易見的,高性能,輕量級,易用的api,以及眾多的使用者,都為這個框架注入了可靠的因素。截止目前為止,github上面已經有了 35,994 star. 一個開源框架,關注數越多,就會越可靠,因為大家會在使用當中不斷地對它進行改進。

下面放幾個鏈接方便進行查看:

幾個流行的go框架進行比較

go幾大web框架比較 這個主頁對幾大web框架進行了一些比較,主要是統計了github star last commit time 等等信息,可以作為一個參考。

幾大優勢

  • 速度快: 高性能,無反射代碼,低內存消耗
  • 中間件(攔截器): 可以更優雅的實現請求鏈路上下文的控制,比如日志,身份驗證等等
  • Crash保活: 當一個請求掛掉之后,並不影響服務器的穩定運行
  • 數據驗證
  • 分組的API管理: 當需要給特定請求加驗證,一些請求又不需要的時候,可以很方便的實現
  • 錯誤管理
  • 簡單易用而豐富的類型支持: Json, Xml, Html 等等

簡單的使用

引入項目

現在有方便的go mod支持,引入變得非常簡單,直接在需要使用的代碼文件處 import "github.com/gin-gonic/gin" 即可

gin的HelloWorld

package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 } 

使用如上代碼,便可輕松啟動一個監聽所有請求,端口為8080(默認) 的服務了。可以嘗試用 curl 進行測試:

$ curl localhost:8080/ping            
output: {"message":"pong"} 

如果想監聽在其它端口,可以進行修改 r.Run("0.0.0.0:9000")

Get 請求以及參數獲取

現在要發起一個請求: curl 'localhost:8080/send?a=1&b=2',現在來看看我們如何通過 *gin.Context 拿到傳參呢,這里我們省去一些代碼

g.GET("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{ "a": ctx.Query("a") "b": ctx.Query("b"), }) }) // output: {"a":"1","b":"2"} 

我們把拿到的參數又返回給了客戶端

假如前端此時需要傳一個數組到服務器,通過GET方式,這時候該怎么辦呢,此時有三個辦法

  • 客戶端 curl 'localhost:8080/send?a=1&a=2' 傳遞同樣的 key, web 框架會當做數組處理
g.GET("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{ "a": ctx.QueryArray("a"), }) }) // output: {"a":["1","2"]} 
  • 客戶端 curl 'localhost:8080/send?a=["1", "2"]' 這里是使用json字符串的形式傳遞數組,注意這里面包含了 url 不允許直接傳輸的字符,比如 [ ] 和 " 等,需要進行url編碼, 可以在 UrlEncode編碼/UrlDecode解碼 - 站長工具 這里進行轉換一下,轉換后的結果如下:
    curl 'localhost:8080/send?a=%5b%221%22%2c+%222%22%5d', gin 相關代碼如下:
g.GET("/send", func(ctx *gin.Context) { out := []string{} err := json.Unmarshal([]byte(ctx.Query("a")), &out) if err != nil { ctx.JSON(200, gin.H{ "error": err.Error(), }) return } ctx.JSON(200, gin.H{ "a": out, }) }) // output: {"a":["1","2"]} 
  • 第三種就是傳遞參數的時候,傳遞一個字符串,每個元素之間用,號等分割一下,在服務端取到該字符串之后,再利用strings.Split()函數分割成數組即可,這里就不例舉代碼了。

NOTE: 如果 query 取的key不存在,會得到什么呢?答案是空字符串,或者你也可以使用
func (c *Context) GetQuery(key string) (string, bool) 這個方法,可以返回一個 bool 用來判斷是否存在

路徑參數Path該如何獲取

curl 'localhost:8080/send/1?b=2'

g.GET("/send/:id", func(ctx *gin.Context) { ctx.JSON(200, gin.H{ "id": ctx.Param("id"), "b": ctx.Query("b"), }) }) // output: {"b":"2","id":"1"} 

同樣,如果獲取不到則為空字符串,如果路徑參數忘了傳,則url匹配不上,就會報404

Post 請求及其參數獲取

眾所周知,post請求,傳輸的數據是會在body里面的,在gin里面是怎么獲取的呢
curl -XPOST 'localhost:8080/send?a=1' -d "b=2&c=3", 這里也帶上了 query parameter

g.POST("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{ "a": ctx.Query("a"), "b": ctx.PostForm("b"), "c": ctx.PostForm("c"), }) }) // output: {"a":"1","b":"2","c":"3"} 

同樣,如果參數不存在,也是獲取到空字符串

模型綁定

gin提供了模型綁定,方便參數的規范化,簡單來說,模型綁定就是把參數解析出來,放在你定義好的結構體里面。模型綁定的好處如下

  • 規范化數據
  • 能夠將string數據解析為你希望的類型,比如 uint32
  • 能夠使用參數驗證器

最常使用的模型綁定方法

gin 對模型綁定出錯的處理分了兩個大類

  • Bind*方法,以及MustBindWith方法 出錯會將返回code置為400
  • ShouldBind* 方法,出錯不會設置返回code,可以自己控制返回的code,一般來說,直接調 ShouldBind方法就行了,它會自動判斷 Content-Type 選擇相應的綁定

Query Param 綁定

請求為 curl 'localhost:8080/send?a=haha&b=123', go代碼如下

g.GET("/send", func(ctx *gin.Context) { type Param struct { A string `form:"a" binding:"required"` B int `form:"b" binding:"required"` } param := new(Param) if err := ctx.ShouldBind(param); err != nil { ctx.JSON(400, gin.H{ "err": err.Error(), }) return } ctx.JSON(200, gin.H{ "Content-Type": ctx.ContentType(), "a": param.A, "b": param.B, }) }) // output: {"Content-Type":"","a":"haha","b":123} 

如果什么都不傳,因為設置了 binding:"required" 這個tag,於是在綁定最后驗證時候,會報錯

Query 與 Form Param 同時綁定

請求為 curl -XPOST 'localhost:8080/send?a=haha' -d "b=2&c=3", go代碼如下

g.POST("/send", func(ctx *gin.Context) { type Param struct { A string `form:"a" binding:"required"` B int `form:"b" binding:"required"` C int `form:"c" binding:"required"` } param := new(Param) if err := ctx.ShouldBind(param); err != nil { ctx.JSON(400, gin.H{ "err": err.Error(), }) return } ctx.JSON(200, gin.H{ "a": param.A, "b": param.B, "c": param.C, }) }) // output: {"a":"haha","b":2,"c":3} 

可以看到,Query 和 Form 參數都是用的 form 這個tag

Path 路徑參數綁定

上面看到了,Query 和 Form 是可以綁定到一個結構體當中,但是路徑參數就只能單獨進行綁定了(如果不需要使用參數驗證,則直接用 ctx.Param(key)方法即可),需要單獨綁定到一個結構體當中, 使用ctx.ShouldBindUri() 這個方法進行綁定。

請求為 curl 'localhost:8080/send/haha', go代碼如下

g.GET("/send/:name", func(ctx *gin.Context) { type Param struct { A string `uri:"name" binding:"required"` } param := new(Param) if err := ctx.ShouldBindUri(param); err != nil { ctx.JSON(200, gin.H{ "err": err.Error(), }) return } ctx.JSON(200, gin.H{ "a": param.A, }) }) 

如果覺得綁定到2個結構體很麻煩,可以自己實現 Binding 接口,然后使用自己實現的Bind方法即可

模型綁定方法總結

強制綁定

  • func (c *Context) MustBindWith(obj interface{}, b binding.Binding) error 通用的強制綁定方法,出錯則置返回code為400,一般不直接用此方法
  • func (c *Context) Bind(obj interface{}) error 調用 MustBindWith 自動根據請求類型來判斷綁定
  • func (c *Context) BindHeader(obj interface{}) error 調用 MustBindWith 綁定請求頭,tag使用header
  • func (c *Context) BindJSON(obj interface{}) error 調用 MustBindWith 綁定json,tag使用json
  • func (c *Context) BindQuery(obj interface{}) error 調用 MustBindWith 綁定 Query Param,tag使用form
  • func (c *Context) BindUri(obj interface{}) error 調用 MustBindWith 綁定Path路徑參數,tag使用uri
  • func (c *Context) BindXML(obj interface{}) error 調用 MustBindWith 綁定xml,tag使用xml
  • func (c *Context) BindYAML(obj interface{}) error 調用 MustBindWith 綁定yaml,tag使用yaml

非強制綁定

  • func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error 通用的綁定方法
  • func (c *Context) ShouldBind(obj interface{}) error 調用 ShouldBindWith 自動根據請求類型判斷綁定
  • 其余方法這里不列出,都是在上述方法基礎上加 Should 並且都是調用 ShouldBindWith, 下面說兩個不一樣的

這里說一個需要注意的問題,如果是數據存儲於 Body 里面的,gin是封裝的標准庫的http,而 Body 是io.ReadCloser 類型的,只能讀取一次,之后就關閉,內容只允許讀一次,也就是說,上述的 Bind 凡是讀 Body 的,都不能再讀第二次,這個可以用其他辦法解決,這里暫且只說一個,那就是
func (c *Context) ShouldBindBodyWith(obj interface{}, bb binding.BindingBody) (err error) 方法,這個方法允許調用多次,因為它將內容暫時存在了 gin.Context 當中,比如綁定json如下代碼所示:
ctx.ShouldBindBodyWith(&objA, binding.JSON)

還有個注意點就是,綁定的結構體,如果包含有子結構體,對於 form 傳參來說,是不會有什么影響的,比如 a=1&b=2&c=3 , a b c 可以分別在不同的結構體中,可以是結構體指針也可以是結構體,具體可以參考 這里

服務器返回

這里總結下服務器返回的方法,不過調用完成之后,記得return
func (c *Context) String(code int, format string, values …interface{}) 返回類型為 string
func (c *Context) JSON(code *int*, obj interface{}) 這個用得最多的,返回 json

還有許多方法,這里不一一列舉,可以參考 gin 源碼學習

中間件(或叫攔截器)

中間件是在請求前后做一些事情,比如驗證登錄,打印日志等等工作,可以將接口邏輯划分開來,與業務代碼分離,下面看看中間件是怎么使用的

中間件函數的定義其實和普通請求接口的定義是一樣的,都是 type HandlerFunc func(*Context),中間件分為以下三類作用域

  • 全局中間件
  • group中間件
  • 單個接口級別的中間件

中間件的作用順序是,定義在前面的先生效,也就是定義在前面的會先調用,而且可以定義多個中間件

全局中間件: 對所有請求接口都有效
group中間件: 對該組的接口有效
單個接口級別中間件: 只對該接口有效

現在介紹幾個gin自帶的全局中間件,還記得初始化gin的時候,調用的哪個方法嗎,就是 gin.Default(),下面看看它的源碼

func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine } 

可以看到,也是調用 New() 這個函數構造了 Engine 對象,並且初始化了 2 個中間件,一個用於日志打印,另一個用於崩潰恢復,這2個都是全局中間件

gin主頁的README有段代碼,清晰的解釋了這三種中間件的定義

func main() { // 使用New()初始化 r := gin.New() // 全局中間件: 日志打印 r.Use(gin.Logger()) // 全局中間件: r.Use(gin.Recovery()) // 單個接口的中間件 r.GET("/benchmark", MyBenchLogger(), benchEndpoint) authorized := r.Group("/") // 分組中間件: 只對該組接口有效 authorized.Use(AuthRequired()) { authorized.POST("/login", loginEndpoint) authorized.POST("/submit", submitEndpoint) authorized.POST("/read", readEndpoint) testing := authorized.Group("testing") testing.GET("/analytics", analyticsEndpoint) } r.Run(":8080") } 

中間件彼此形成一條鏈條,對於每個請求來說,它的調用關系如下圖:

  • 在中間件內部調用 ctx.Next() 即是調用鏈條的下一級方法,比如,在全局中間件里調用 Next,則表示調用 group中間件函數,這就可以使用切面編程思想,把鏈條下一級函數看做一個切面,然后在前后做一些事情,比如計算接口的調用時間等。
  • 如果不顯示調用Next,則該中間件函數執行完之后,會執行鏈條的下一級函數
  • 如果想要中斷鏈條,則調用ctx.Abort() 函數,調用之后,會正常執行完當前中間件函數,但是不會再執行鏈條下一級了,而是准備返回接口。

舉個例子

一般來說,定義一個中間件,都遵循下面這種風格,YourFunc() HandlerFunc 返回這個處理函數的方式,當然你也可以直接定義一個 HandlerFunc 也是可以的。

現在要實現一個功能,能夠計算某個請求的耗時,使用中間件來完成,代碼如下

timeCalc := func() gin.HandlerFunc { return func(ctx *gin.Context) { if ctx.Query("a") == "" { ctx.Abort() // 終止調用鏈條 ctx.JSON(http.StatusBadRequest, gin.H{ "message": "a參數有問題,請檢查參數", }) return } start := time.Now() // Next 在這里相當於 接口函數,在Next之前則在接口函數之前執行 fmt.Println("Next之前") ctx.Next() fmt.Println("Next之后") cost := time.Since(start) // Next 之后,則相當於在接口函數之后執行,形成了一個切面 fmt.Printf("用時 %d 微秒\n", cost.Microseconds()) } } g.GET("/send", timeCalc(), func(ctx *gin.Context) { fmt.Println("進入接口函數") ctx.JSON(http.StatusOK, gin.H{ "a": ctx.Query("a"), }) }) // 服務端輸出: // Next之前 // 進入接口函數 // Next之后 // 用時 231 微秒 

NOTE: 如果需要在接口鏈條的某一處,開辟一個gorutine進行處理,如果需要用到 gin.Context 的話,需要調用 ctx.Copy() 函數進行一份拷貝,然后在開辟的gorutine當中使用該拷貝

Gin一些開源中間件 這里可以找到一些比較實用的中間件,可以自己探索下

MODE

目前Gin有三種模式: debug release test 三種,可以通過設置 GIN_MODE 這個環境變量來控制
比如現在需要將這個web應用發布到正式環境,那么需要將生產機器上的gin的環境變量設置為 release: export GIN_MODE= release

在debug模式下,會在開頭多一些打印

單元測試

下面這個例子可以參考一下

package main import ( "encoding/json" "io/ioutil" "net/http" "strings" "testing" "github.com/stretchr/testify/assert" ) func do(t *testing.T, req *http.Request) ([]byte, error) { resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } assert.Equal(t, 200, resp.StatusCode) return body, nil } // curl 'localhost:8080/send?a=1&b=2' func TestGet(t *testing.T) { req, _ := http.NewRequest("GET", "http://localhost:8080/send?a=1&b=2", nil) body, err := do(t, req) assert.NoError(t, err) assert.Equal(t, "{\"a\":\"1\",\"b\":\"2\"}\n", string(body)) } // curl -XPOST 'localhost:8080/send' -H 'Content-Type: application/json' -d '{"a":1,"b":2,"c":3}' func TestPost(t *testing.T) { req, _ := http.NewRequest("POST", "http://localhost:8080/send", strings.NewReader(`{"a":1,"b":2,"c":3}`)) req.Header.Set("Content-Type", "application/json") // 傳json記得修改 body, err := do(t, req) assert.NoError(t, err) type Resp struct { A int `json:"a"` B int `json:"b"` C int `json:"c"` } resp := new(Resp) assert.NoError(t, json.Unmarshal(body, resp)) assert.Equal(t, &Resp{ A: 1, B: 2, C: 3, }, resp) } 

當你的代碼嵌套比較多,並且不易於在單元測試當中去啟動這個服務的時候,可以使用這個方法,單元測試就相當於開了一個http client,去請求已啟動的服務,這時候需要先啟動項目的服務,才能調用單元測試哦。

下面介紹一個獨立的,也是gin源碼經常使用的這種測試方法,可以獨立運行,不依賴於已啟動的服務

func TestIndependent0(t *testing.T) { w := httptest.NewRecorder() // 用於返回的數據 ctx, _ := gin.CreateTestContext(w) // 模擬返回數據 ctx.JSON(http.StatusOK, gin.H{ "a": 1, }) assert.Equal(t, "{\"a\":1}\n", string(w.Body.Bytes())) } func TestIndependent1(t *testing.T) { w := httptest.NewRecorder() _, router := gin.CreateTestContext(w) router.GET("/send", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "a": 1, }) }) router.ServeHTTP(w, httptest.NewRequest("GET", "http://localhost:8080/send", nil)) t.Log(string(w.Body.Bytes())) // output: {"a":1} }

參數驗證

我們知道,一個請求完全依賴前端的參數驗證是不夠的,需要前后端一起配合,才能萬無一失,下面介紹一下,在Gin框架里面,怎么做接口參數驗證的呢

gin 目前是使用 go-playground/validator 這個框架,截止目前,默認是使用 v10 版本;具體用法可以看看 validator package · go.dev 文檔說明哦

下面以一個單元測試,簡單說明下如何在tag里驗證前端傳遞過來的數據

簡單的例子

func TestValidation(t *testing.T) { ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) testCase := []struct { msg string // 本測試用例的說明 jsonStr string // 輸入的參數 haveErr bool // 是否有 error bindStruct interface{} // 被綁定的結構體 errMsg string // 如果有錯,錯誤信息 }{ { msg: "數據正確: ", jsonStr: `{"a":1}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required"` }{}, }, { msg: "數據錯誤: 缺少required的參數", jsonStr: `{"b":1}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"required"` }{}, errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'required' tag", }, { msg: "數據正確: 參數是數字並且范圍 1 <= a <= 10", jsonStr: `{"a":1}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required,max=10,min=1"` }{}, }, { msg: "數據錯誤: 參數數字不在范圍之內", jsonStr: `{"a":1}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"required,max=10,min=2"` }{}, errMsg: "Key: 'A' Error:Field validation for ‘A’ failed on the ‘min’ tag", }, { msg: "數據正確: 不等於列舉的參數", jsonStr: `{"a":1}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required,ne=10"` }{}, }, { msg: "數據錯誤: 不能等於列舉的參數", jsonStr: `{"a":1}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"required,ne=1,ne=2"` // ne 表示不等於 }{}, errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'ne' tag", }, { msg: "數據正確: 需要大於10", jsonStr: `{"a":11}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required,gt=10"` }{}, }, // 總結: eq 等於,ne 不等於,gt 大於,gte 大於等於,lt 小於,lte 小於等於 { msg: "參數正確: 長度為5的字符串", jsonStr: `{"a":"hello"}`, haveErr: false, bindStruct: &struct { A string `json:"a" binding:"required,len=5"` // 需要參數的字符串長度為5 }{}, }, { msg: "參數正確: 為列舉的字符串之一", jsonStr: `{"a":"hello"}`, haveErr: false, bindStruct: &struct { A string `json:"a" binding:"required,oneof=hello world"` // 需要參數是列舉的其中之一,oneof 也可用於數字 }{}, }, { msg: "參數正確: 參數為email格式", jsonStr: `{"a":"hello@gmail.com"}`, haveErr: false, bindStruct: &struct { A string `json:"a" binding:"required,email"` }{}, }, { msg: "參數錯誤: 參數不能等於0", jsonStr: `{"a":0}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"gt=0|lt=0"` }{}, errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'gt=0|lt=0' tag", }, // 詳情參考: https://pkg.go.dev/github.com/go-playground/validator/v10?tab=doc } for _, c := range testCase { ctx.Request = httptest.NewRequest("POST", "/", strings.NewReader(c.jsonStr)) if c.haveErr { err := ctx.ShouldBindJSON(c.bindStruct) assert.Error(t, err) assert.Equal(t, c.errMsg, err.Error()) } else { assert.NoError(t, ctx.ShouldBindJSON(c.bindStruct)) } } } // 測試 form 的情況 // time_format 這個tag 只能在 form tag 下能用 func TestValidationForm(t *testing.T) { ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) testCase := []struct { msg string // 本測試用例的說明 formStr string // 輸入的參數 haveErr bool // 是否有 error bindStruct interface{} // 被綁定的結構體 errMsg string // 如果有錯,錯誤信息 }{ { msg: "數據正確: 時間格式", formStr: `a=2010-01-01`, haveErr: false, bindStruct: &struct { A time.Time `form:"a" binding:"required" time_format:"2006-01-02"` }{}, }, } for _, c := range testCase { ctx.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(c.formStr)) ctx.Request.Header.Add("Content-Type", binding.MIMEPOSTForm) // 這個很關鍵 if c.haveErr { err := ctx.ShouldBind(c.bindStruct) assert.Error(t, err) assert.Equal(t, c.errMsg, err.Error()) } else { assert.NoError(t, ctx.ShouldBind(c.bindStruct)) } } } 

簡單解釋一下,還記得上一篇文章講的單元測試嗎,這里只需要使用到 gin.Context 對象,所以忽略掉 gin.CreateTestContext()返回的第二個參數,但是需要將輸入參數放進 gin.Context,也就是把 Request 對象設置進去 ,接下來才能使用 Bind 相關的方法哦。

其中 binding: 代替框架文檔中的 validate,因為gin單獨給驗證設置了tag名稱,可以參考gin源碼 binding/default_validator.go

func (v *defaultValidator) lazyinit() { v.once.Do(func() { v.validate = validator.New() v.validate.SetTagName("binding") // 這里改為了 binding }) } 

上面的單元測試已經把基本的驗證語法都列出來了,剩余的可以根據自身需求查詢文檔進行的配置

日志

使用gin默認的日志

首先來看看,初始化gin的時候,使用了 gin.Deatult() 方法,上一篇文章講過,此時默認使用了2個全局中間件,其中一個就是日志相關的 Logger() 函數,返回了日志處理的中間件

這個函數是這樣定義的

func Logger() HandlerFunc { return LoggerWithConfig(LoggerConfig{}) } 

繼續跟源碼,看來真正處理的就是 LoggerWithConfig() 函數了,下面列出部分關鍵源碼

func LoggerWithConfig(conf LoggerConfig) HandlerFunc { formatter := conf.Formatter if formatter == nil { formatter = defaultLogFormatter } out := conf.Output if out == nil { out = DefaultWriter } notlogged := conf.SkipPaths isTerm := true if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) { isTerm = false } var skip map[string]struct{} if length := len(notlogged); length > 0 { skip = make(map[string]struct{}, length) for _, path := range notlogged { skip[path] = struct{}{} } } return func(c *Context) { // Start timer start := time.Now() path := c.Request.URL.Path raw := c.Request.URL.RawQuery // Process request c.Next() // Log only when path is not being skipped if _, ok := skip[path]; !ok { // 中間省略這一大塊是在處理打印的邏輯 // …… fmt.Fprint(out, formatter(param)) // 最后是通過 重定向到 out 進行輸出 } } } 

稍微解釋下,函數入口傳參是 LoggerConfig 這個定義如下:

type LoggerConfig struct { Formatter LogFormatter Output io.Writer SkipPaths []string } 

而調用 Default() 初始化gin時候,這個結構體是一個空結構體,在 LoggerWithConfig 函數中,如果這個結構體內容為空,會為它設置一些默認值
默認日志輸出是到 stdout 的,默認打印格式是由 defaultLogFormatter 這個函數變量控制的,如果想要改變日志輸出,比如同時輸出到文件stdout,可以在調用 Default() 之前,設置 DefaultWriter 這個變量;但是如果需要修改日志格式,則不能調用 Default() 了,可以調用 New() 初始化gin之后,使用 LoggerWithConfig() 函數,將自己定義的 LoggerConfig 傳入。

使用第三方的日志

默認gin只會打印到 stdout,我們如果使用第三方的日志,則不需要管gin本身的輸出,因為它不會輸出到文件,正常使用第三方的日志工具即可。由於第三方的日志工具,我們需要實現一下 gin 本身打印接口(比如接口時間,接口名稱,path等等信息)的功能,所以往往需要再定義一個中間件去打印。

logrus

GitHub主頁

logrus 是一個比較優秀的日志框架,下面這個例子簡單的使用它來記錄下日志

func main() { g := gin.Default() gin.DisableConsoleColor() testLogrus(g) if err := g.Run(); err != nil { panic(err) } } func testLogrus(g *gin.Engine) { log := logrus.New() file, err := os.Create("mylog.txt") if err != nil { fmt.Println("err:", err.Error()) os.Exit(0) } log.SetOutput(io.MultiWriter(os.Stdout, file)) logMid := func() gin.HandlerFunc { return func(ctx *gin.Context) { var data string if ctx.Request.Method == http.MethodPost { // 如果是post請求,則讀取body body, err := ctx.GetRawData() // body 只能讀一次,讀出來之后需要重置下 Body if err != nil { log.Fatal(err) } ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 重置body data = string(body) } start := time.Now() ctx.Next() cost := time.Since(start) log.Infof("方法: %s, URL: %s, CODE: %d, 用時: %dus, body數據: %s", ctx.Request.Method, ctx.Request.URL, ctx.Writer.Status(), cost.Microseconds(), data) } } g.Use(logMid()) // curl 'localhost:8080/send' g.GET("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"msg": "ok"}) }) // curl -XPOST 'localhost:8080/send' -d 'a=1' g.POST("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"a": ctx.PostForm("a")}) }) } 

zap

zap文檔
zap同樣是比較優秀的日志框架,是由uber公司主導開發的,這里就不單獨舉例子了,可與參考下 zap中間件 的實現

 
分類:  python2019
標簽:  linuxpython
標簽:  httpgoweb
 


免責聲明!

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



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