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}
}

最后

自定義日志以及一些別的應用,留到下一篇文章


免責聲明!

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



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