Gin 簡介
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance -- up to 40 times faster. If you need smashing performance, get yourself some Gin.
-- 這是來自 github 上 Gin 的簡介
Gin 是一個用 Go 寫的 HTTP web 框架,它是一個類似於 Martini 框架,但是 Gin 用了 httprouter 這個路由,它比 martini 快了 40 倍。如果你追求高性能,那么 Gin 適合。
當然 Gin 還有其它的一些特性:
- 路由性能高
- 支持中間件
- 路由組
- JSON 驗證
- 錯誤管理
- 可擴展性
Gin 文檔:
Gin 快速入門 Demo
我以前也寫過一些關於 Gin 應用入門的 demo,在這里。
Gin v1.7.0 , Go 1.16.11
官方的一個 quickstart:
package main
import "github.com/gin-gonic/gin"
// https://gin-gonic.com/docs/quickstart/
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // 監聽在默認端口8080, 0.0.0.0:8080
}
上面就完成了一個可運行的 Gin 程序了。
分析上面的 Demo
第一步:gin.Default()
Engine struct 是 Gin 框架里最重要的一個結構體,包含了 Gin 框架要使用的許多字段,比如路由(組),配置選項,HTML等等。
New() 和 Default() 這兩個函數都是初始化 Engine 結構體。
RouterGroup struct 是 Gin 路由相關的結構體,路由相關操作都與這個結構體有關。

- A. Default() 函數
這個函數在 gin.go/Default(),它實例化一個 Engine,調用 New() 函數:
// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L180
// 實例化 Engine,默認帶上 Logger 和 Recovery 2 個中間件,它是調用 New()
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
debugPrintWARNINGDefault() // debug 程序
engine := New() // 新建 Engine 實例,原來 Default() 函數是最終是調用 New() 新建 engine 實例
engine.Use(Logger(), Recovery()) // 使用一些中間件
return engine
}
Engine 又是什么?
- B. Engine struct 是什么和 New() 函數:
Engine 是一個 struct 類型,里面包含了很多字段,下面代碼只顯示主要字段:
// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L57
// gin 中最大的一個結構體,存儲了路由,設置選項和中間件
// 調用 New() 或 Default() 方法實例化 Engine struct
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
RouterGroup // 組路由(路由相關字段)
... ...
HTMLRender render.HTMLRender
FuncMap template.FuncMap
allNoRoute HandlersChain
allNoMethod HandlersChain
noRoute HandlersChain
noMethod HandlersChain
pool sync.Pool
trees methodTrees
maxParams uint16
trustedCIDRs []*net.IPNet
}
type HandlersChain []HandlerFunc
gin.go/New() 實例化 gin.go/Engine struct,簡化的代碼如下:
這個 New 函數,就是初始化 Engine struct,
// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L148
// 初始化 Engine,實例化一個 engine
// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
FuncMap: template.FuncMap{},
... ...
trees: make(methodTrees, 0, 9),
delims: render.Delims{Left: "{{", Right: "}}"},
secureJSONPrefix: "while(1);",
}
engine.RouterGroup.engine = engine // RouterGroup 里的 engine 在這里賦值,下面分析 RouterGroup 結構體
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
- C. RouterGroup
gin.go/Engine struct 里的 routergroup.go/RouterGroup struct 這個與路由有關的字段,它也是一個結構體,代碼如下:
//https://github.com/gin-gonic/gin/blob/v1.7.0/routergroup.go#L41
// 配置存儲路由
// 路由后的處理函數handlers(中間件)
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {
Handlers HandlersChain // 存儲處理路由
basePath string
engine *Engine // engine
root bool
}
// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L34
type HandlersChain []HandlerFunc
https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L31
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
第二步:r.GET()
r.GET() 就是路由注冊和路由處理handler。
routergroup.go/GET(),handle() -> engine.go/addRoute()
// https://github.com/gin-gonic/gin/blob/v1.7.0/routergroup.go#L102
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
handle 處理函數:
// https://github.com/gin-gonic/gin/blob/v1.7.0/routergroup.go#L72
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()
}
combineHandlers() 函數把所有路由處理handler合並起來。
addRoute() 這個函數把方法,URI,處理handler 加入進來, 這個函數主要代碼如下:
// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L276
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
... ...
// 每一個http method(GET, POST, PUT...)都構建一顆基數樹
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)
... ...
}
上面這個root.addRoute函數在 tree.go 里,而這里的代碼多數來自 httprouter 這個路由庫。
gin 里號稱 40 times faster。
到底是怎么做到的?
httprouter 路由數據結構Radix Tree
httprouter文檔
在 httprouter 文檔里,有這樣一句話:
The router relies on a tree structure which makes heavy use of common prefixes, it is basically a compact prefix tree (or just Radix tree)
用了 prefix tree 前綴樹 或 Radix tree 基數樹。與 Trie 字典樹有關。
Radix Tree 叫基數特里樹或壓縮前綴樹,是一種更節省空間的 Trie 樹。
Trie 字典樹
Trie,被稱為前綴樹或字典樹,是一種有序樹,其中的鍵通常是單詞和字符串,所以又有人叫它單詞查找樹。
它是一顆多叉樹,即每個節點分支數量可能為多個,根節點不包含字符串。
從根節點到某一節點,路徑上經過的字符連接起來,為該節點對應的字符串。
除根節點外,每一個節點只包含一個字符。
每個節點的所有子節點包含的字符都不相同。
優點:利用字符串公共前綴來減少查詢時間,減少無謂的字符串比較
Trie 樹圖示:

(為 b,abc,abd,bcd,abcd,efg,hii 這7個單詞創建的trie樹, https://baike.baidu.com/item/字典樹/9825209)
trie 樹的代碼實現:https://baike.baidu.com/item/字典樹/9825209#5
Radix Tree基數樹
認識基數樹:
Radix Tree,基數特里樹或壓縮前綴樹,是一種更節省空間的 Trie 樹。它對 trie 樹進行了壓縮。
看看是咋壓縮的,假如有下面一組數據 key-val 集合:
{
"def": "redisio",
"dcig":"mysqlio",
"dfo":"linux",
"dfks":"tdb",
"dfkz":"dogdb",
}
用上面數據中的 key 構造一顆 trie 樹:

現在壓縮 trie 樹(Compressed Trie Tree)中的唯一子節點,就可以構建一顆 radix tree 基數樹。
父節點下第一級子節點數小於 2 的都可以進行壓縮,把子節點合並到父節點上,把上圖 <2 子節點數壓縮,變成如下圖:

把 c,f 和 c,i,g 壓縮在一起,這樣就節省了一些空間。壓縮之后,分支高度也降低了。
這個就是對 trie tree 進行壓縮變成 radix tree。
在另外看一張出現次數比較多的 Radix Tree 的圖:

(圖Radix_tree 來自:https://en.wikipedia.org/wiki/Radix_tree)
基數樹唯一子節點都與其父節點合並,邊沿(edges)既可以存儲多個元素序列也可以存儲單個元素。比如上圖的 r, om,an,e。
基數樹的圖最下面的數字對應上圖的排序數字,比如
,就是 ruber 字符,
。
什么時候使用基數樹合適:
字符串元素個數不是很多,且有很多相同前綴時適合使用基數樹這種數據結構。
基數樹的應用場景:
httprouter 中的路由器。
使用 radix tree 來構建 key 為字符串的關聯數組。
很多構建 IP 路由也用到了 radix tree,比如 linux 中,因為 ip 通常有大量相同前綴。
Redis 集群模式下存儲 slot 對應的所有 key 信息,也用到了 radix tree。文件 rax.h/rax.c 。
radix tree 在倒排索引方面使用也比較廣。
httprouter中的基數樹
node 節點定義:
// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L46
type node struct {
path string // 節點對應的字符串路徑
wildChild bool // 是否為參數節點,如果是參數節點,那么 wildChild=true
nType nodeType // 節點類型,有幾個枚舉值可以看下面nodeType的定義
maxParams uint8 // 節點路徑最大參數個數
priority uint32 // 節點權重,子節點的handler總數
indices string // 節點與子節點的分裂的第一個字符
children []*node // 子節點
handle Handle // http請求處理方法
}
節點類型 nodeType 定義:
// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L39
// 節點類型
const (
static nodeType = iota // default, 靜態節點,普通匹配(/user)
root // 根節點
param // 參數節點(/user/:id)
catchAll // 通用匹配,匹配任意參數(*user)
)
indices 這個字段是緩存下一子節點的第一個字符。
比如路由: r.GET("/user/one"), r.GET("/user/two"), indices 字段緩存的就是下一節點的第一個字符,即 "ot" 2個字符。這個就是對搜索匹配進行了優化。

如果 wildChild=true,參數節點時,indices=""。
addRoute 添加路由:
addRoute(),添加路由函數,這個函數代碼比較多,
分為空樹和非空樹時的插入。
空樹時直接插入:
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root // 節點 nType 是 root 類型
非空樹的處理:
先是判斷樹非空(non-empty tree),接着下面是一個 for 循環,下面所有的處理都在 for 循環面。
-
更新 maxParams 字段
-
尋找共同的最長前綴字符
// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L100 // Find the longest common prefix. 尋找字符相同前綴,用 i 數字表示 // This also implies that the common prefix contains no ':' or '*',表示沒有包含特殊匹配 : 或 * // since the existing key can't contain those chars. i := 0 max := min(len(path), len(n.path)) for i < max && path[i] == n.path[i] { i++ } -
split edge 開始分裂節點
比如第一個路由 path 是 user,新增一個路由 uber,u 就是它們共同的部分(common prefix),那么就把 u 作為父節點,剩下的 ser,ber 作為它的子節點
// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L107 // Split edge if i < len(n.path) { child := node{ path: n.path[i:], // 上面已經判斷了匹配的字符共同部分用i表示,[i:] 從i開始計算取字符剩下不同部分作為子節點 wildChild: n.wildChild, // 節點類型 nType: static, // 靜態節點普通匹配 indices: n.indices, children: n.children, handle: n.handle, priority: n.priority - 1, // 節點降級 } // Update maxParams (max of all children) for i := range child.children { if child.children[i].maxParams > child.maxParams { child.maxParams = child.children[i].maxParams } } n.children = []*node{&child} // 當前節點的子節點修改為上面剛剛分裂的節點 // []byte for proper unicode char conversion, see #65 n.indices = string([]byte{n.path[i]}) n.path = path[:i] n.handle = nil n.wildChild = false } -
i<len(path),將新節點作為子節點插入
https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L137
- 4.1 n.wildChild = true,對特殊參數節點的處理 ,: 和 *
// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L133
if n.wildChild {
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// Adding a child to a catchAll is not possible
n.nType != catchAll &&
// Check for longer wildcard, e.g. :name and :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
} else {
// Wildcard conflict
var pathSeg string
... ...
}
}
- 4.2 開始處理 indices
// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L171
c := path[0] // 獲取第一個字符
// slash after param,處理nType為參數的情況
if n.nType == param && c == '/' && len(n.children) == 1
// Check if a child with the next path byte exists
// 判斷子節點是否和當前path匹配,用indices字段來判斷
// 比如 u 的子節點為 ser 和 ber,indices 為 u,如果新插入路由 ubb,那么就與子節點 ber 有共同部分 b,繼續分裂 ber 節點
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// Otherwise insert it
// indices 不是參數和通配匹配
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
}
// 新增子節點
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handle)
-
i=len(path)路徑相同
如果已經有handler處理函數就報錯,沒有就賦值handler
insertChild 插入子節點:
getValue 路徑查找:
上面2個函數可以獨自分析下 - -!
可視化radix tree操作
https://www.cs.usfca.edu/~galles/visualization/RadixTree.html
radix tree 的算法操作可以看這里,動態展示。
參考
- https://github.com/gin-gonic/gin/tree/v1.7.0 gin 源碼
- https://github.com/julienschmidt/httprouter httprouter地址
- https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go httprouter tree源碼
- https://gin-gonic.com/docs/quickstart/ gin doc
- https://www.cs.usfca.edu/~galles/visualization/RadixTree.html radix tree算法步驟可視化
- https://baike.baidu.com/item/字典樹/9825209 百科基數樹
- 《算法》 5.2 單詞查找樹 trie tree 作者: Robert Sedgewick / Kevin Wayne
- https://en.wikipedia.org/wiki/Trie 維基trie樹(en)
- https://en.wikipedia.org/wiki/Radix_tree 維基radix tree(en)
- https://zh.wikipedia.org/wiki/基數樹 維基基數樹(zh)
- https://github.com/redis/redis/blob/6.0.14/src/rax.c redis 中 radix tree 使用
- https://github.com/redis/redis/blob/6.0.14/src/rax.h redis 中 radix tree 使用
