gin源码阅读之四 -- gin的路由算法

2019-03-04
3分钟阅读时长

gin的是路由算法其实就是一个Radix Tree. 有关数据结构的可以自己去网上找相关资料查看.

注册路由预处理

我们在使用gin时通过下面的代码注册路由

普通注册

router.POST("/somePost", func(context *gin.Context) {
	context.String(http.StatusOK, "some post")
})

使用中间件

router.Use(Logger())

使用Group

v1 := router.Group("v1")
{
	v1.POST("login", func(context *gin.Context) {
		context.String(http.StatusOK, "v1 login")
	})
}

这些操作, 最终都会在反应到gin的路由树上

具体实现

// routergroup.go:L72-77
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()
}

在调用POST, GET, HEAD等路由HTTP相关函数时, 会调用handle函数

如果调用了中间件的话, 会调用下面函数

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

如果使用了Group的话, 会调用下面函数

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
	return &RouterGroup{
		Handlers: group.combineHandlers(handlers),
		basePath: group.calculateAbsolutePath(relativePath),
		engine:   group.engine,
	}
}

重点关注下面两个函数:

// routergroup.go:L208-217
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	if finalSize >= int(abortIndex) {
		panic("too many handlers")
	}
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	return mergedHandlers
}
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
	return joinPaths(group.basePath, relativePath)
}

func joinPaths(absolutePath, relativePath string) string {
	if relativePath == "" {
		return absolutePath
	}

	finalPath := path.Join(absolutePath, relativePath)
	appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'
	if appendSlash {
		return finalPath + "/"
	}
	return finalPath
}

joinPaths函数里面有段代码, 很有意思, 我还以为是写错了. 主要是path.Join的用法.

finalPath := path.Join(absolutePath, relativePath)
appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/'

在当路由是/user/这种情况就满足了lastChar(relativePath) == '/' && lastChar(finalPath) != '/'. 主要原因是path.Join(absolutePath, relativePath)之后, finalPath是user

综合来看, 在预处理阶段

  1. 在调用中间件的时候, 是将某个路由的handler处理函数和中间件的处理函数都放在了Handlers的数组中
  2. 在调用Group的时候, 是将路由的path上面拼上Group的值. 也就是/user/:name, 会变成v1/user:name

真正注册

// routergroup.go:L72-77
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()
}

调用group.engine.addRoute(httpMethod, absolutePath, handlers)将预处理阶段的结果注册到gin Engine的trees上

gin路由树简单介绍

gin的路由树算法是一棵前缀树. 不过并不是只有一颗树, 而是每种方法(POST, GET …)都有自己的一颗树

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)
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers)
}

gin 路由最终的样子大概是下面的样子

type node struct {
	path      string
	indices   string
	children  []*node
	handlers  HandlersChain
	priority  uint32
	nType     nodeType
	maxParams uint8
	wildChild bool
}

其实gin的实现不像一个真正的树, children []*node所有的孩子都放在这个数组里面, 利用indices, priority变相实现一棵树

获取路由handler

当服务端收到客户端的请求时, 根据pathtrees匹配到相关的路由, 拿到相关的处理handlers

...
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
	if t[i].method != httpMethod {
		continue
	}
	root := t[i].root
	// Find route in tree
	handlers, params, tsr := root.getValue(path, c.Params, unescape) // 看这里
	if handlers != nil {
		c.handlers = handlers
		c.Params = params
		c.Next()
		c.writermem.WriteHeaderNow()
		return
	}
	if httpMethod != "CONNECT" && path != "/" {
		if tsr && engine.RedirectTrailingSlash {
			redirectTrailingSlash(c)
			return
		}
		if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
			return
		}
	}
	break
}
...

主要在下面这个函数里面调用程序注册的路由处理函数

func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

gin的路由采用的前缀树, 由这里还想到有后缀树, B树, B-树, B+树, 平衡树, 红黑树, 这些树都是面试中经常问到的. 后面开专门的文章介绍这些树