Golang之Gin框架源碼解讀——第一章

Gin是使用Go語言編寫的高性能的web服務框架,根據官方的測試,性能是httprouter的40倍左右。要使用好這套框架呢,首先我們就得對這個框架的基本結構有所瞭解,所以我將從以下幾個方面來對Gin的源碼進行解讀。

  • 第一章:Gin是如何儲存和映射URL路徑到相應的處理函數的
  • 第二章:Gin中間件的設計思想及其實現
  • 第三章:Gin是如何解析客戶端發送請求中的參數的
  • 第四章:Gin是如何將各類格式(JSON/XML/YAML等)數據解析

Gin Github官方地址

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

從這裏函數一看,咦?這不就是綁定中間件方法嗎?從這裏我們就可以看出RouterGroupHandlers是用於儲存中間件函數的。除此之外,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方法被儲存到了enginetrees中去了。

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的節點的子節點的處理函數會失效,所以需要多加註意。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章