Gin
是使用Go語言編寫的高性能的web
服務框架,根據官方的測試,性能是httprouter
的40倍左右。要使用好這套框架呢,首先我們就得對這個框架的基本結構有所瞭解,所以我將從以下幾個方面來對Gin
的源碼進行解讀。
- 第一章:
Gin
是如何儲存和映射URL
路徑到相應的處理函數的 - 第二章:
Gin
中間件的設計思想及其實現 - 第三章:
Gin
是如何解析客戶端發送請求中的參數的 - 第四章:
Gin
是如何將各類格式(JSON/XML/YAML
等)數據解析
Gin是如何組織和映射URL到處理函數的
在談這個之前我們先以官方給的示例代碼來作爲我們的切入口,然後一步一步的看Gin
是如何處理的,瞭解Gin
的深層次工作原理。
func main() {
router := gin.Default()
// 這個處理函數將會匹配 /user/john 但不會匹配 /user/ 或者 /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
// 這個路徑將會匹配 /user/john/ 或者 /user/john/send
// 如果沒有匹配到 /user/john, 那麼將會被重定向到 /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
router.POST("/user/:name/*action", func(c *gin.Context) {
log.Println(c.FullPath() == "/user/:name/*action") // true
})
router.Run(":8080")
}
首先,我們要通過gin.Default()
獲取到一個Engine
對象指針,然後由router
來負責將各個映射路徑記錄到處理的映射關係。我們先來看下Engine
的數據結構是怎樣的。
type Engine struct {
//中間件信息就存儲在這個裏面
RouterGroup
//是否啓動自動重定向。例如:配置Handler時是/foo,
//但實際發送的請求是/foo/。在啓用本選項後,將會重定向到/foo
RedirectTrailingSlash bool
// 是否啓動請求路由修復功能。
// 啓用過後,當/../foo找不到匹配路由時,會自動刪除..部分路由,然後重新匹配知道找到匹配路由,如上路由就會被匹配到/foo
RedirectFixedPath bool
//啓用後,如果找不到當前路由匹配的HTTP方法,
//而在其他HTTP方法中能找到,則返回405響應碼。
HandleMethodNotAllowed bool
//是否獲取真正的客戶端IP,而不是代理服務器IP(nginx等),
// 開啓後將會從"X-Real-IP和X-Forwarded-For"中解析得到客戶端IP
ForwardedByClientIP bool
//啓用後將在頭部加入"X-AppEngine"標識,以便與PaaS集成
AppEngine bool
//啓用後,將使用原有的URL.rawPath(沒有對轉義字符進行處理的,
// 如%/+等)地址來進行解析,而不是使用URL.path來解析,默認爲false
UseRawPath bool
//如果啓用,則路徑中的轉義字符將不會被轉義
UnescapePathValues bool
//設置用來緩存客戶端發送的文件的緩衝區大小,默認:32MB
MaxMultipartMemory int64
//啓用後將會刪除多餘的分隔符"/"
RemoveExtraSlash bool
//用於保存tmpl文件中用於引用變量的定界符,默認是"{{}}",
// 調用r.Delims("{[{", "}]}")可以修改
delims render.Delims
//設置防止JSON劫持,在json字符串前加的邏輯代碼,
//默認是:"while(1);"
secureJsonPrefix string
//html文件解析器
HTMLRender render.HTMLRender
//tmpl文件的內建函數列表,可以在tmpl文件中調用函數,使用
//router.SetFuncMap(template.FuncMap{
// "formatAsDate": formatAsDate,
//})可設置
FuncMap template.FuncMap
// HandlersChain就是func(*Context)數組
// 以下四個調用鏈中保存的就是在不同情況下回調的處理函數
// 找不到匹配路由(404)
allNoRoute HandlersChain
//返回405狀態時會回調
allNoMethod HandlersChain
//沒有配置路由時回調,主要是代碼測試時候使用的
noRoute HandlersChain
//沒有配置映射方法時回調,主要是代碼測試時候使用的
noMethod HandlersChain
//連接池用於保存與客戶端的連接上下文(Context)
pool sync.Pool
//路徑搜索樹,代碼中配置的路由信息都以樹節點的形式組織起來
// 下面會詳細介紹
trees methodTrees
}
在大致瞭解完Gin
的核心對象Engine
之後,我們腦海裏就可以有一個大致的結構了。因爲整個框架都是爲繞着Engine
來進行編寫的,下面我們就重點介紹一下Engine
中的幾個比較重要的結構,來更深入地瞭解Gin
。
RouterGroup
type RouterGroup struct {
Handlers HandlersChain
basePath string
//注意這裏存在交叉依賴
engine *Engine
root bool
}
光看這個RouterGroup
結構體,我們彷彿還不太清楚他的作用是什麼,不過我們通過一下幾個方法就可以知道了。
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
從這裏函數一看,咦?這不就是綁定中間件方法嗎?從這裏我們就可以看出RouterGroup
的Handlers
是用於儲存中間件函數的。除此之外,RouterGroup
還實現了IRoutes
接口。
type IRoutes interface {
Use(...HandlerFunc) IRoutes
Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes
StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}
看到這裏是不是很眼熟,對了,這就是官方示例中綁定路由信息的函數。例如:
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
那麼我們接下來就選擇GET
函數來詳細講解一下,其他函數類似:
首先我們來看一下GET
函數原型
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
這裏簡單對比一下GET
函數和POST
函數就會發現,他們都是調用的handle
方法,那麼我們來看下handle
方法。
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
//計算最簡潔的相對路徑,去除多餘符號
absolutePath := group.calculateAbsolutePath(relativePath)
//合併處理函數,就是講我們自己編寫的處理函數與中間件函數連接成一個處理鏈,
//這樣在路由匹配時不僅會調用我們編寫的函數,也會調用中間件函數
handlers = group.combineHandlers(handlers)
//像向engine對象添加路由信息
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
從上面我們可以看出,路由信息經過一系列的處理,最終還是通過addRoute
方法被儲存到了engine
的trees
中去了。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
//此處省略參數校驗
//打印配置的路由信息
debugPrintRoute(method, path, handlers)
//獲取對應方法的根樹,engine爲每個HTTP方法(GET/POST/DELET...)都生成了一個根樹
root := engine.trees.get(method)
//如果根樹爲空,則說明還未給該HTTP方法生成過搜索樹
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
//獲取到根樹之後再調用真正的儲存函數
root.addRoute(path, handlers)
}
在瞭解真正的儲存函數之前,我們需要先看一下Gin
內置的根樹
結構體,以便更好的瞭解路由信息的儲存過程。
路由根樹
首先我們就從獲取根樹的函數開始。
root := engine.trees.get(method)
從這裏我們可以看到根樹,是從engine.trees
獲取到的,而engine.trees
是如下的一個類型:
type methodTrees []methodTree
type methodTree struct {
// HTTP方法名
method string
//真正的根節點指針
root *node
}
而樹節點的數據結構如下:
type node struct {
//當前節點所達路徑
path string
//用於記錄子節點的與當前節點的最長公共前綴的後一個字符
//例如:當前節點爲/(根節點),有兩個子節點,全路徑分別爲:
// /user、/form_table
//那麼當前節點的indices就是"uf",用於快速索引到子節點的
// 如果當前節點
indices string
//子節點指針數組
children []*node
//當前節點的處理鏈
handlers HandlersChain
//匹配優先級,一般按照最長路徑匹配原則設置
priority uint32
//節點類型
nType nodeType
//當前節點與子節點中所有參數的個數
// 參數指的是REST參數,而不是GET/POST中提交的參數
maxParams uint8
//子節點是否爲通配符節點
wildChild bool
//達到當前節點的完整路徑
fullPath string
}
type nodeType uint8
const (
//靜態路由信息節點,默認值
static nodeType = iota
//根節點
root
//參數節點
param
//表示當前節點已經包含所有的REST參數了
catchAll
)
真正的存儲函數解讀如下:
func (n *node) addRoute(path string, handlers HandlersChain) {
//記錄完整的路徑
fullPath := path
//隨着匹配路徑的增長優先級逐漸增大
n.priority++
//計算當前傳入路徑中有多少參數
numParams := countParams(path)
//如果當前節點爲空,則生成一個新的根節點,並以此更新當前空節點
// Empty tree
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
return
}
//初始化父節點的路徑長度
// 主要是看傳入的路徑和當前節點是否有公共前綴
parentFullPathIndex := 0
walk:
for {
//如果當前路徑中的參數個數大於父節點中記錄的參數數目則更新爲大值
// 因爲父節點記錄的是自己和子節點中所有參數的最大個數
if numParams > n.maxParams {
n.maxParams = numParams
}
//尋找公共前綴的下標索引
i := longestCommonPrefix(path, n.path)
//如果當前節點的路徑長度大於公共前綴的下標索引
// 則說明當前節點路徑和新加入的路徑不存在包含關係
// (/user、/user/:name這樣就有包含關係)
// 需要分裂成兩個子節點,例如:
// /user /form_table
// 最初當前節點路徑爲/user,傳入的新路徑爲/form_table
// 則可計算得公共前綴的下標索引爲1
// 則當前節點的路徑更新爲"/"
// 並分裂成兩個子節點,路徑分別爲"user"、"form_table"
// 這第一步就是將當前節點分裂成一個子節點
if i < len(n.path) {
child := node{
path: n.path[i:],
wildChild: n.wildChild,
indices: n.indices,
children: n.children,
handlers: n.handlers,
//由於分裂並不會增長匹配路徑所以優先級不會增加
//這裏減一主要是在這個函數的開始部分默認就會+1
//所以需要減掉
priority: n.priority - 1,
fullPath: n.fullPath,
}
//更新當前節點的最大參數值
for _, v := range child.children {
if v.maxParams > child.maxParams {
child.maxParams = v.maxParams
}
}
n.children = []*node{&child}
//這裏就更新成了公共前綴的後一個字符了
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
}
//這一步就是分裂成兩個子節點中的第二步
//將新加入的路徑生成相應子節點,加入剛纔被分裂的父節點當中去
if i < len(path) {
path = path[i:]
//判斷當前節點是否是通配符節點,如:*name、:name
if n.wildChild {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// 檢查當前路徑是否還未遍歷完
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
//如果發現還有子路可以遍歷則遞歸
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue walk
}
}
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(path, "/", 2)[0]
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
panic("'" + pathSeg +
"' in new path '" + fullPath +
"' conflicts with existing wildcard '" + n.path +
"' in existing prefix '" + prefix +
"'")
}
c := path[0]
//如果當前節點是參數節點,且有一個子節點則遞歸遍歷
if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0]
n.priority++
continue walk
}
//這個是查看有沒有現存的子節點與傳入路徑相匹配,
// 有則進入該子節點進行遞歸
for i, max := 0, len(n.indices); i < max; i++ {
if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
//如果傳入路徑非":"、"*"開頭則說明是普通靜態節點
// 直接構造後插入,並添加子節點索引
if c != ':' && c != '*' {
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
fullPath: fullPath,
}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handlers)
return
}
//如果當前節點已經有處理函數,則說明之前已經有註冊過這個路由了,發出警告,並更新處理函數
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
return
}
}
在瞭解完樹節點的數據結構和構造過程之後,我們以下面這段代碼爲例看看在根樹生成後的大致結構是什麼樣的。
func main() {
router := gin.Default()
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action+","
c.String(http.StatusOK, message)
})
if err := router.Run();err != nil {
log.Println("something error");
}
}
這裏需要說明的是
:
通配符表示當前參數必須存在,而*
表示該參數可有可無。當indices
爲空時,則表示當前節點只有一個子節點,且wildchild
爲true,此時是用不上indices
的,直接取索引0.
我們先看紅框①這個節點匹配的是/user/:name
這種情況,調用的是第一個處理函數,即:
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})
而紅框②這個節點匹配的是/user/:name/
(即*action
參數爲空)中特殊情況,調用的是第二個處理函數。同時由於indices
爲空,還表示其下還有一個非空的匹配節點,匹配的是/user/:name/*action
這種。
發現的問題
在研讀Gin的接受代碼時,我發現如下一種現象:
func main() {
router := gin.Default()
router.GET("/user/:name/*action", func(c *gin.Context) {
log.Println("/user/:name/*action");
})
router.GET("/user/:name/*action/other", func(c *gin.Context) {
log.Println("/user/:name/*action/other");
})
if err := router.Run();err != nil {
log.Println("something error");
}
}
在運行上述代碼之後,你會發現無論你是發送/user/name/action
還是發送/user/name/action/other
,控制檯始終都是打印/user/name/action
。即如下這條路由規則是無效的。
router.GET("/user/:name/*action/other", func(c *gin.Context) {
log.Println("/user/:name/*action/other");
})
通過Debug
模式我們可以看到,雖然生成了/other
節點,同時也具有handlers
處理函數,但是由於/action
節點的類型爲catchAll
,在路徑搜索時,到/action
節點後就認爲參數匹配完畢了,就直接返回了,所以調用的是/action
節點的handlers
處理函數。
雖然Restful API
接口規範中並沒有對這種情況進行特別說明,不過平時開發也難免會寫出這種形式的請求定製,尤其是Spring
的老手,這種情況會導致所有catchAll
的節點的子節點的處理函數會失效,所以需要多加註意。