go 进阶 gin底层原理相关: 二. gin的路由注册原理

文章详细介绍了Gin框架如何基于httprouter库实现路由注册,包括前缀树的概念、节点结构以及路由注册的具体过程。通过对Gin源码的分析,揭示了Gin如何构建和管理路由树,以及处理不同类型的节点(如静态、参数、通配符节点)。
摘要由CSDN通过智能技术生成

一. httprouter

  1. gin底层实际是基于httprouter库实现, 我们先了解一下httprouter:go 进阶 http标准库相关: 七. 高性能可扩展 HTTP 路由 httprouter

网上有说gin在路由注册时是基于julienschmidt/httprouter实现的,有调用过julienschmidt/httprouter内部代码,但是我看github.com/gin-gonic/gin v1.9.1版本路由注册相关源码时并没有找到调用julienschmidt/httprouter为止,不知道是我扣的不够深还是后续版本进行了其它优化,还是gin并没有实际使用julienschmidt/httprouter,只是参考自己实现了一套

二. 了解前缀树与node结构

  1. Trie树,又叫字典树、前缀树(Prefix Tree),是一种多叉树结构,是一个由“路径”和“节点”组成多叉树结构。由根节点出发,按照每个字符,创建对应字符路径
  2. Trie的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的,但是内存消耗大
  3. 前缀树的3个基本性质:
  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
  3. 每个节点的所有子节点包含的字符都不相同
  1. gin的路由树算法类似于多个前缀树,每种方法(POST, GET ,PATCH…)都有自己的一颗树
  2. 例如,路由的地址是添加 ‘a’ 字符,根节点 pass++ , 由于字符串还没有结束,后面还有 ‘b’、‘c’,end 不变,并在路的另一端创建一个节点(因为两个节点才能形成一条路),并将新节点的 pass++,end 同样不变
/hi
/hello
/:name/:id

在这里插入图片描述

  1. gin 是基于httprouter 实现的,最终会将路由函数与对应的请求路径关系封装为一个node,以节点的形式存储到前缀树上,先了解一个node结构
type node struct {
	//当前节点路径的值。(路径部分前缀)
    path      string
    //包含子节点首字符的前缀索引,比如 r 下游两个子节点 om 和 ub,则r节点的indices为 ou
    indices   string
    //当前节点下的子节点列表
    children  []*node
    handlers  HandlersChain
    priority  uint32
    //节点类型,可为static, root,param 或 catchAll
    nType     nodeType
    maxParams uint8
    //当前节点是否有一个带参数的子节点
    wildChild bool
    //当前节点完全路径的值。(路径完整前缀)
    fullPath  string
}

三. 构建前缀树,注册路由的过程

  1. 在我们编写服务时,调用RouterGroup的GET/POST等方法注册路由,以GET方法为例,查看方法源码:
  1. 方法内部会调用RouterGroup的handle()方法
  2. 继续查看handle()方法,该方法内部会调用calculateAbsolutePath()获取绝对路径
  3. 然后调用combineHandlers()创建一个新的handler切片,获取中间件相关的Handlers,插入新切片的头部,再把用户自定义处理某路径下请求的handler插入到尾部(比较重要的一个步骤,关系到中间件)
    会回去RouterGroup中的Engine属性
  4. 执行Engine的addRoute()方法,根据 HTTP 方法获取到对应的方法树,如果方法树不存在,则创建一颗方法树,添加到 Engine 的 trees 上,将路由函数与请求路径封装为methodTree
  5. 最后在方法树上,调用node下的 addRoute 创建路径节点
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	//1.通过相对路径获取绝对路径
    absolutePath := group.calculateAbsolutePath(relativePath)
    //2.合并handlers,在combineHandlers方法内部会生成一个新的handler切片
    //先把中间件的handler插入到头部,
    //再把用户自定义处理某路径下请求的handler插入到尾部
    //最后返回的是这个新生成的切片
    handlers = group.combineHandlers(handlers)
    //3.构建前缀树,将Handlers添加到前缀树中
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}

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
}

// ============== gin.go ===============
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)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    //此处的root是node类型,调用node下的addRoute()方法
    root.addRoute(path, handlers)
}
  1. 每一个HTTP Method(GET、POST、PUT、DELETE…)都对应了一棵 radix tree,在注册路由时会先根据请求方法获取对应的树,封装为methodTrees类型,以切片的形式存储到树中
type methodTree struct {
	method string //请求方法Get, Post, Put ....
	root   *node //节点
}
type methodTrees []methodTree  // slice

封装并添加Node节点

  1. 继续查看node下的addRoute()方法
// tree.go

// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++
	numParams := countParams(path)  // 数一下参数个数

	// 空树就直接插入当前节点
	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)

		// 分裂边缘(此处分裂的是当前树节点)
		// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分
		// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点
		if i < len(n.path) {
			child := node{
				path:      n.path[i:],  // 公共前缀后的部分作为子节点
				wildChild: n.wildChild,
				indices:   n.indices,
				children:  n.children,
				handlers:  n.handlers,
				priority:  n.priority - 1, //子节点优先级-1
				fullPath:  n.fullPath,
			}

			// Update maxParams (max of all children)
			for _, v := range child.children {
				if v.maxParams > child.maxParams {
					child.maxParams = v.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.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// 将新来的节点插入新的parent节点作为子节点
		if i < len(path) {
			path = path[i:]

			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)] {
					// 检查更长的通配符, 例如 :name and :names
					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 +
					"'")
			}
			// 取path首字母,用来与indices做比较
			c := path[0]

			// 处理参数后加斜线情况
			if n.nType == param && c == '/' && len(n.children) == 1 {
				parentFullPathIndex += len(n.path)
				n = n.children[0]
				n.priority++
				continue walk
			}

			// 检查路path下一个字节的子节点是否存在
			// 比如s的子节点现在是earch和upport,indices为eu
			// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点
			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 != '*' {
				// []byte for proper unicode char conversion, see #65
				// 注意这里是直接拼接第一个字符到n.indices
				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
	}
}
  1. insertChild函数是根据path本身进行分割,将/分开的部分分别作为节点保存,形成一棵树结构。参数匹配中的:和*的区别是,前者是匹配一个字段而后者是匹配后面所有的路径
// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
  // 找到所有的参数
	for numParams > 0 {
		// 查找前缀直到第一个通配符
		wildcard, i, valid := findWildcard(path)
		if i < 0 { // 没有发现通配符
			break
		}

		// 通配符的名称必须包含':' 和 '*'
		if !valid {
			panic("only one wildcard per path segment is allowed, has: '" +
				wildcard + "' in path '" + fullPath + "'")
		}

		// 检查通配符是否有名称
		if len(wildcard) < 2 {
			panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
		}

		// 检查这个节点是否有已经存在的子节点
		// 如果我们在这里插入通配符,这些子节点将无法访问
		if len(n.children) > 0 {
			panic("wildcard segment '" + wildcard +
				"' conflicts with existing children in path '" + fullPath + "'")
		}

		if wildcard[0] == ':' { // param
			if i > 0 {
				// 在当前通配符之前插入前缀
				n.path = path[:i]
				path = path[i:]
			}

			n.wildChild = true
			child := &node{
				nType:     param,
				path:      wildcard,
				maxParams: numParams,
				fullPath:  fullPath,
			}
			n.children = []*node{child}
			n = child
			n.priority++
			numParams--

			// 如果路径没有以通配符结束
			// 那么将有另一个以'/'开始的非通配符子路径。
			if len(wildcard) < len(path) {
				path = path[len(wildcard):]

				child := &node{
					maxParams: numParams,
					priority:  1,
					fullPath:  fullPath,
				}
				n.children = []*node{child}
				n = child  // 继续下一轮循环
				continue
			}

			// 否则我们就完成了。将处理函数插入新叶子中
			n.handlers = handlers
			return
		}

		// catchAll
		if i+len(wildcard) != len(path) || numParams > 1 {
			panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
		}

		if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
			panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
		}

		// currently fixed width 1 for '/'
		i--
		if path[i] != '/' {
			panic("no / before catch-all in path '" + fullPath + "'")
		}

		n.path = path[:i]
		
		// 第一个节点:路径为空的catchAll节点
		child := &node{
			wildChild: true,
			nType:     catchAll,
			maxParams: 1,
			fullPath:  fullPath,
		}
		// 更新父节点的maxParams
		if n.maxParams < 1 {
			n.maxParams = 1
		}
		n.children = []*node{child}
		n.indices = string('/')
		n = child
		n.priority++

		// 第二个节点:保存变量的节点
		child = &node{
			path:      path[i:],
			nType:     catchAll,
			maxParams: 1,
			handlers:  handlers,
			priority:  1,
			fullPath:  fullPath,
		}
		n.children = []*node{child}

		return
	}

	// 如果没有找到通配符,只需插入路径和句柄
	n.path = path
	n.handlers = handlers
	n.fullPath = fullPath
}
  1. node结构体解释
// node表示用于高效路由的树结构中的一个节点。这个树结构基本上是一个基数树(或紧凑前缀树)。
type node struct {
	// path是用于与这个节点匹配的URL路径的一部分。
	// 它可以是一个固定的字符串,也可以是一个参数(以':'开头)
	// 或者是一个通配符(以'*'开头)。
	path string

	// indices是一个字符串,包含了这个节点所有可能的子节点的第一个字符。
	// 它用于在树中匹配路径时快速查找下一个子节点。
	indices string

	// wildChild表示这个节点是否有一个带有通配符路径的子节点。
	wildChild bool

	// nType表示这个节点是什么类型的节点。
	nType nodeType

	// priority表示这个节点相对于它的兄弟节点有多么“重要”。
	// 它用于在插入新节点时对一个节点的子节点进行排序,以便在树中匹配路径时优先尝试优先级更高的节点。
	priority uint32

	// children是这个节点的子节点,按照优先级排序。
	children []*node

	// handlers是与这个节点关联的HTTP处理函数,按照HTTP方法索引。
	handlers Handlers

	// fullPath是从根节点到这个节点的完整路径,只在调试时使用。
	fullPath string
}

四. 总结

  1. gin在路由注册时底层基于httprouter库实现, 要了解httprouter(但是我没找到相关源码不知道是不是我扣的不够深)
  2. 在我们编写gin服务时,首先会调用gin.New()或gin.Default()创建Engine 也就是gin引擎实例,要了解Engine内部结构,在Engine 中
  1. 提供了一个trees属性,是一个methodTree数组,内部保存了接口路径与处理器函数的映射关系,也就是路由树
  2. Engine上还绑定了一个addRoute()方法,当调用该方法时,会将接口路径与接口处理器函数封装为methodTree保存到Engine的trees中
  3. 我们看一下methodTree内部包含一个string类型的method属性用来保存当前接口的方法比如get,post等,node结构体类型的root属性路由链表,实际接口路径与处理器函数会封装为node
  1. 还要了解一下Engine 的继承关系,Engine隐式的继承了RouterGroup,那Engine实际就可以看成一个RouterGroup,而RouterGroup实现了IRouter,IRoutes接口拥有GET(),POST()等路由注册方法,以GET()方法为例内部会调用RouterGroup的handle()方法,查看这个方法源码重点:
  1. RouterGroup的combineHandlers()方法,获取跟当前接口路径相关的中间件Handlers,与当前接口的处理器Handler合并为一个Handlers列表
  2. 调用Engine的addRoute()方法,将处理器Handlers列表包括中间件,请求方法,接口路径进行封装,实现路由注册
  1. 查看Engine的addRoute()方法:
  1. 获取Engine的trees属性,根据请求方法比如GET,POST等在trees获取到root节点,如果没有则封装methodTree创建root根节点保存到trees中,自此我们知道路由注册时请求方法是跟节点
  2. 然后调用root的addRoute()方法也就是node结构体上的addRoute()方法,封装node,将根节点到当前这个节点的路径存储到node的fullPath属性上,将当前节点需要执行的handler存储到node的handlers属性上,维护链表关系,设置node的children属性,最终在将这个node封装为methodTree存储到trees树中
  1. 查看底层node节点的nType属性表示当前节点的类型,得出节点有:
  1. staticAny:表示静态节点,该节点对应的路由路径中不包含参数。
  2. param:表示参数节点,该节点对应的路由路径中包含参数。
  3. catchAll:表示通配符节点,该节点对应的路由路径中包含通配符。通配符节点一般用于匹配REST API中的路径参数。
  4. root:表示根节点,该节点没有任何意义,只是作为树形结构的根节点存在。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值