golang框架-web框架之gin

gin介紹

gin是一個 Web應用框架,擁有良好的性能和簡單明瞭的接口。同時支持中間件,類型綁定等實用功能。node

爲何要用gin

在實際開發中,不多會直接實用http.Server。而本身搭建框架有必定成本,同時沒有通過系統的校驗,容易出現問題。而現有的框架中,gin擁有良好的性能,更重要的是接口清晰明瞭,接入成本極低。同時,其支持的功能也是多種多樣,如中間件,類型綁定,日誌規範。git

gin 性能

如下是從官網拿到的性能對比指標表github

  • (1): Total Repetitions achieved in constant time, higher means more confident result
  • (2): Single Repetition Duration (ns/op), lower is better
  • (3): Heap Memory (B/op), lower is better
  • (4): Average Allocations per Repetition (allocs/op), lower is better
Benchmark name (1) (2) (3) (4)
BenchmarkGin_GithubAll 30000 48375 0 0
BenchmarkAce_GithubAll 10000 134059 13792 167
BenchmarkBear_GithubAll 5000 534445 86448 943
BenchmarkBeego_GithubAll 3000 592444 74705 812
BenchmarkBone_GithubAll 200 6957308 698784 8453

接入栗子

簡單示例

接受/ping路徑的Get請求,並返回message:"pong"bash

package main

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

func main() {
	router := gin.Default()
	router.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	router.Run() // listen and serve on 0.0.0.0:8080
}
複製代碼

支持全部的http協議

router := gin.Default()
router.GET("/someGet", getting)
router.POST("/somePost", posting)
router.PUT("/somePut", putting)
router.DELETE("/someDelete", deleting)
router.PATCH("/somePatch", patching)
router.HEAD("/someHead", head)
router.OPTIONS("/someOptions", options)
複製代碼

參數解析

router := gin.Default()
// The request responds to a url matching:  /welcome?firstname=Jane&lastname=Doe
/**** example1: 解析Querystring ****/
router.GET("/welcome", func(c *gin.Context) {
    firstname := c.DefaultQuery("firstname", "Guest")
    // shortcut for c.Request.URL.Query().Get("lastname")
    lastname := c.Query("lastname") 
    c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
})

/**** example2: 解析表單 ****/
router.POST("/form_post", func(c *gin.Context) {
	message := c.PostForm("message")
	nick := c.DefaultPostForm("nick", "anonymous")

	c.JSON(200, gin.H{
	    "status":  "posted",
		"message": message,
		"nick":    nick,
	})
})
複製代碼

gin特性

路由組

支持以組爲單位的路由,下面栗子就是以/v1開頭,以/v2開頭的兩組配置。路由組能夠共享一樣的配置,好比路由組v1能夠使用中間件a。而v2能夠使用另外一箇中間件,互不影響。數據結構

router := gin.Default()

// Simple group: v1
v1 := router.Group("/v1")
{
	v1.POST("/login", loginEndpoint)
	v1.POST("/submit", submitEndpoint)
	v1.POST("/read", readEndpoint)
}

// Simple group: v2
v2 := router.Group("/v2")
{
	v2.POST("/login", loginEndpoint)
	v2.POST("/submit", submitEndpoint)
	v2.POST("/read", readEndpoint)
}
router.Run(":8080")
複製代碼

中間件

中間件是對框架能力一個很是好的抽象。以組件的形式,爲路由或路由組提供插件式功能。也能夠本身實現中間件,加入到Use中來。app

// Creates a router without any middleware by default
r := gin.New()

// Global middleware
r.Use(gin.Logger())
r.Use(gin.Recovery())

// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
複製代碼

中間件能夠很是方便的定義日誌格式框架

router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
    // your custom format
	return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
			param.ClientIP,
			param.TimeStamp.Format(time.RFC1123),
			param.Method,
			param.Path,
			param.Request.Proto,
			param.StatusCode,
			param.Latency,
			param.Request.UserAgent(),
			param.ErrorMessage,
		)
}))
複製代碼

數據綁定

使用 c.ShouldBind方法,能夠將參數自動綁定到 struct.該方法是會檢查 Url 查詢字符串和 POST 的數據,並且會根據 content-type類型,優先匹配JSON或者 XML,以後纔是 Form.ide

// 定義一個 Person 結構體,用來綁定數據
type Person struct {
    Name     string    `form:"name"`
    Address  string    `form:"address"`
    Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

func main() {
    route := gin.Default()
    route.GET("/testing", startPage)
    route.Run(":8085")
}

func startPage(c *gin.Context) {
    var person Person
    // 綁定到 person
    if c.ShouldBind(&person) == nil {
        log.Println(person.Name)
        log.Println(person.Address)
        log.Println(person.Birthday)
    }

    c.String(200, "Success")
}
複製代碼

gin原理分析

gin 能夠說全是在handler上作文章。下面咱們就以這三句話,一探gin。post

func main() {
    r := gin.Default()
    r.GET("/getb", GetDataB)
    r.Run()
}
複製代碼

生成默認引擎

r := gin.Default() 的定義以下性能

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

Engine是gin中的一個很重要的概念。等下面對.r.Run分析時候,咱們會發現他的本質就是http.Server裏面的handler實例!

這裏看到engine.Use(Logger(), Recovery()) 直觀上就很像以前提早的http中間件的某種實現 Logger()是日誌中間件, Recovery()是針對panic的中間件(否則每一個handler都得寫個panic處理邏輯)

來分析一下Recover中間件

func Recovery() HandlerFunc {
	return RecoveryWithWriter(DefaultErrorWriter)
}

func RecoveryWithWriter(out io.Writer) HandlerFunc {
	var logger *log.Logger
	if out != nil {
		logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
	}
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
               // 省略非關鍵代碼
			}
		}()
		c.Next()
	}
}
複製代碼

c.Next()能將多箇中間件串聯起來調用

// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }    
}
複製代碼

c.handlersc.index即當前索引位置對應的handler的調用

type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
複製代碼

因爲處理邏輯是放在了c.Next前面,因此中間件的處理順序是先入後出。中間件自己應該互相獨立。但若是由於特殊緣由,有先後依賴,就要注意這點。

註冊路由規則

r.GET("/getb", GetDataB) 實現以下

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, 
                   handlers ...HandlerFunc) IRoutes {
   return group.handle("GET", relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, 
                               handlers HandlersChain) IRoutes {
   absolutePath := group.calculateAbsolutePath(relativePath)
   handlers = group.combineHandlers(handlers)
   group.engine.addRoute(httpMethod, absolutePath, handlers)
   return group.returnObj()
}

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   assert1(path[0] == '/', "path must begin with '/'")
   assert1(method != "", "HTTP method can not be empty")
   assert1(len(handlers) > 0, "there must be at least one handler")

   debugPrintRoute(method, path, handlers)
   root := engine.trees.get(method)
   if root == nil {
   	root = new(node)
   	root.fullPath = "/"
   	engine.trees = append(engine.trees, 
   	    methodTree{method: method, root: root})
   }
   root.addRoute(path, handlers)
}
複製代碼

能夠看到,路由映射是加入了一顆樹中。這裏使用的是radix樹,Radix樹,即基數樹,也稱壓縮前綴樹,是一種提供key-value存儲查找的數據結構。與Trie不一樣的是,它對Trie樹進行了空間優化,只有一個子節點的中間節點將被壓縮。一樣的,Radix樹的插入、查詢、刪除操做的時間複雜度都爲O(k)。存儲原理示意圖以下:

啓動服務

看下r.Run的實現

func (engine *Engine) Run(addr ...string) (err error) {
   defer func() { debugPrintError(err) }()

   address := resolveAddress(addr)
   debugPrint("Listening and serving HTTP on %s\n", address)
   err = http.ListenAndServe(address, engine)
   return
}
複製代碼

能夠看到,實際調用的,仍是http.ListenAndServe這個方法,engine做爲handler參數傳入。http.ListenAndServe的原理,在前文httpServer有過闡述,這裏不作過多分析。

總結

gin有以下特色:

  • 接入成本很是低,做爲一個組件,這是最重要的一點
  • 擁有強大的中間件功能,用戶能夠自主定製須要的功能
  • 因爲使用了radix樹,路由的性能很高。
  • 數據綁定,讓用戶能夠很是方便地從請求中獲取想要的結構體。

gin對外接口和代碼實現都很是優秀,不管是項目使用,仍是源碼學習,都值得推薦。

相關文章
相關標籤/搜索