作為一款企業級生產力的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置為400ShouldBind*方法,出錯不會設置返回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使用headerfunc (c *Context) BindJSON(obj interface{}) error調用MustBindWith綁定json,tag使用jsonfunc (c *Context) BindQuery(obj interface{}) error調用MustBindWith綁定 Query Param,tag使用formfunc (c *Context) BindUri(obj interface{}) error調用MustBindWith綁定Path路徑參數,tag使用urifunc (c *Context) BindXML(obj interface{}) error調用MustBindWith綁定xml,tag使用xmlfunc (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}
}
最后
自定義日志以及一些別的應用,留到下一篇文章
