1. 程式人生 > 實用技巧 >gin框架的路由原始碼解析

gin框架的路由原始碼解析

前言

本文轉載至 https://www.liwenzhou.com/posts/Go/read_gin_sourcecode/ 可以直接去原文看, 比我這裡直觀

我這裡只是略微的修改

正文

gin的路由實現

使用 Radix Tree , 簡潔版的字首樹

字首樹

別名: 字典樹 / 單詞查詢樹 / 鍵樹

為什麼使用字首樹

  • url是有限的,不可能無限長

  • url是有規律的

  • url是一級一級的, restful 更是如此

比如部落格有的是按年和月分割 /2020/3/aaaa.html /2020/3/bbbb.html 此時使用字首樹更合適

gin的路由樹

基數樹/PAT位樹, 是一種更節省空間的字首樹, 對於基數樹的每個節點,如果該節點是唯一的子樹的話,就和父節點合併。

  • 越常匹配的字首, 權重越大
  • 因為字首樹的構建模式導致越長的路徑定位的時間越長, gin在註冊路由時越長的路由排的越前, 如果最長的節點能優先匹配, 那麼路由匹配所花的時間不一定比短路由更長

gin首先按照請求型別(POST/GET/...), 分為多個PAT樹, 每個PAT樹儲存這個請求型別下面註冊的路由, 路由又根據權重進行排序

路由樹節點

路由樹由一個個節點組成, gin的路由樹節點由結構體 node 表示, 其構造結構如下

// tree.go

type node struct {
   // 節點路徑,比如上面的s,earch,和upport
	path      string
	// 和children欄位對應, 儲存的是分裂的分支的第一個字元
	// 例如search和support, 那麼s節點的indices對應的"eu"
	// 代表有兩個分支, 分支的首字母分別是e和u
	indices   string
	// 兒子節點
	children  []*node
	// 處理函式鏈條(切片)
	handlers  HandlersChain
	// 優先順序,子節點、子子節點等註冊的handler數量
	priority  uint32
	// 節點型別,包括static, root, param, catchAll
	// static: 靜態節點(預設),比如上面的s,earch等節點
	// root: 樹的根節點
	// catchAll: 有*匹配的節點
	// param: 引數節點
	nType     nodeType
	// 路徑上最大引數個數
	maxParams uint8
	// 節點是否是引數節點,比如上面的:post
	wildChild bool
	// 完整路徑
	fullPath  string
}

請求的方法樹

在gin的路由中, 每一個 HTTP Method (GET/POST/PUT/....) 都對應了一棵PAT樹, 在註冊路由時會呼叫 addRoute 函式

// gin.go
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
   
   // 獲取請求方法對應的樹
	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)
}

而在gin中, 每一個 Method 對應的樹關係時是存放在一個切片中, engine.trees

的型別是 methodTrees , 其定義如下

type methodTree struct {
	method string
	root   *node
}

type methodTrees []methodTree  // slice

engine.trees.get 方法如下,(就是for迴圈)

func (trees methodTrees) get(method string) *node {
	for _, tree := range trees {
		if tree.method == method {
			return tree.root
		}
	}
	return nil
}

使用切片而不是使用map來儲存, 可能是考慮到節省記憶體, 而且HTTP請求一共就9種, 使用切片也比較合適, 效率也高, 初始化在gin的 engine

func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		// liwenzhou.com ...
		// 初始化容量為9的切片(HTTP1.1請求方法共9種)
		trees:                  make(methodTrees, 0, 9),
		// liwenzhou.com...
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() interface{} {
		return engine.allocateContext()
	}
	return engine
}

路由匹配

當新的請求進入gin時, 會先經過函式 ServeHTTP

// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  // 這裡使用了物件池
	c := engine.pool.Get().(*Context)
  // 這裡有一個細節就是Get物件後做初始化
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)  // 我們要找的處理HTTP請求的函式

	engine.pool.Put(c)  // 處理完請求後將物件放回池子
}

ServeHTTP 呼叫 handleHTTPRequest 函式(節選)

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {

	// 根據請求方法找到對應的路由樹
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// 在路由樹中根據path查詢
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()  // 執行函式鏈條
			c.writermem.WriteHeaderNow()
			return
		}
	
	c.handlers = engine.allNoRoute
	serveError(c, http.StatusNotFound, default404Body)
}

大致為先COPY一份路由的切片, 先找到與該請求對應的請求型別, 然後在這個請求型別的路由樹種使用 getValue 方法查詢對應的路由, 沒有則返回404