一步一步分析Gin框架路由源码及radix tree基数树
Python微信订餐小程序课程视频
https://edu.csdn.net/course/detail/36074
Python实战量化交易理财系统
https://edu.csdn.net/course/detail/35475
Gin 简介
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API with much better performance – up to 40 times faster. If you need smashing performance, get yourself some Gin.
– 这是来自 github 上 Gin 的简介
Gin 是一个用 Go 写的 HTTP web 框架,它是一个类似于 Martini 框架,但是 Gin 用了 httprouter 这个路由,它比 martini 快了 40 倍。如果你追求高性能,那么 Gin 适合。
当然 Gin 还有其它的一些特性:
- 路由性能高
- 支持中间件
- 路由组
- JSON 验证
- 错误管理
- 可扩展性
Gin 文档:
- https://gin-gonic.com/
- https://github.com/gin-gonic/gin
- https://gin-gonic.com/zh-cn/docs/ 中文文档
Gin 快速入门 Demo
我以前也写过一些关于 Gin 应用入门的 demo,在这里。
Gin v1.7.0 , Go 1.16.11
官方的一个 quickstart:
Copypackage mainimport "github.com/gin-gonic/gin"// https://gin-gonic.com/docs/quickstart/
func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})r.Run() // 监听在默认端口8080, 0.0.0.0:8080
}
上面就完成了一个可运行的 Gin 程序了。
分析上面的 Demo
第一步:gin.Default()
Engine struct 是 Gin 框架里最重要的一个结构体,包含了 Gin 框架要使用的许多字段,比如路由(组),配置选项,HTML等等。
New() 和 Default() 这两个函数都是初始化 Engine 结构体。
RouterGroup struct 是 Gin 路由相关的结构体,路由相关操作都与这个结构体有关。
- A. Default() 函数
这个函数在 gin.go/Default(),它实例化一个 Engine,调用 New() 函数:
Copy// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L180// 实例化 Engine,默认带上 Logger 和 Recovery 2 个中间件,它是调用 New()
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {debugPrintWARNINGDefault() // debug 程序engine := New() // 新建 Engine 实例,原来 Default() 函数是最终是调用 New() 新建 engine 实例engine.Use(Logger(), Recovery()) // 使用一些中间件return engine
}
Engine 又是什么?
- B. Engine struct 是什么和 New() 函数:
gin.go/Engine:
Engine 是一个 struct 类型,里面包含了很多字段,下面代码只显示主要字段:
Copy// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L57// gin 中最大的一个结构体,存储了路由,设置选项和中间件
// 调用 New() 或 Default() 方法实例化 Engine struct
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {RouterGroup // 组路由(路由相关字段)... ...HTMLRender render.HTMLRenderFuncMap template.FuncMapallNoRoute HandlersChainallNoMethod HandlersChainnoRoute HandlersChainnoMethod HandlersChainpool sync.Pooltrees methodTreesmaxParams uint16trustedCIDRs []*net.IPNet
}type HandlersChain []HandlerFunc
gin.go/New() 实例化 gin.go/Engine struct,简化的代码如下:
这个 New 函数,就是初始化 Engine struct,
Copy// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L148// 初始化 Engine,实例化一个 engine
// New returns a new blank Engine instance without any middleware attached.
// By default the configuration is:
// - RedirectTrailingSlash: true
// - RedirectFixedPath: false
// - HandleMethodNotAllowed: false
// - ForwardedByClientIP: true
// - UseRawPath: false
// - UnescapePathValues: true
func New() *Engine {debugPrintWARNINGNew()engine := &Engine{RouterGroup: RouterGroup{Handlers: nil,basePath: "/",root: true,},FuncMap: template.FuncMap{},... ...trees: make(methodTrees, 0, 9),delims: render.Delims{Left: "{{", Right: "}}"},secureJSONPrefix: "while(1);",}engine.RouterGroup.engine = engine // RouterGroup 里的 engine 在这里赋值,下面分析 RouterGroup 结构体engine.pool.New = func() interface{} {return engine.allocateContext()}return engine
}
- C. RouterGroup
gin.go/Engine struct 里的 routergroup.go/RouterGroup struct 这个与路由有关的字段,它也是一个结构体,代码如下:
Copy//https://github.com/gin-gonic/gin/blob/v1.7.0/routergroup.go#L41
// 配置存储路由
// 路由后的处理函数handlers(中间件)
// RouterGroup is used internally to configure router, a RouterGroup is associated with
// a prefix and an array of handlers (middleware).
type RouterGroup struct {Handlers HandlersChain // 存储处理路由basePath stringengine *Engine // engineroot bool
}// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L34
type HandlersChain []HandlerFunchttps://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L31
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
第二步:r.GET()
r.GET() 就是路由注册和路由处理handler。
routergroup.go/GET(),handle() -> engine.go/addRoute()
Copy// https://github.com/gin-gonic/gin/blob/v1.7.0/routergroup.go#L102
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)
}
handle 处理函数:
Copy// https://github.com/gin-gonic/gin/blob/v1.7.0/routergroup.go#L72
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()
}
combineHandlers() 函数把所有路由处理handler合并起来。
addRoute() 这个函数把方法,URI,处理handler 加入进来, 这个函数主要代码如下:
Copy// https://github.com/gin-gonic/gin/blob/v1.7.0/gin.go#L276func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {... ...// 每一个http method(GET, POST, PUT...)都构建一颗基数树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)... ...
}
上面这个root.addRoute函数在 tree.go 里,而这里的代码多数来自 httprouter 这个路由库。
gin 里号称 40 times faster。
到底是怎么做到的?
httprouter 路由数据结构Radix Tree
httprouter文档
在 httprouter 文档里,有这样一句话:
The router relies on a tree structure which makes heavy use of common prefixes, it is basically a compact prefix tree (or just Radix tree)
用了 prefix tree 前缀树 或 Radix tree 基数树。与 Trie 字典树有关。
Radix Tree 叫基数特里树或压缩前缀树,是一种更节省空间的 Trie 树。
Trie 字典树
Trie,被称为前缀树或字典树,是一种有序树,其中的键通常是单词和字符串,所以又有人叫它单词查找树。
它是一颗多叉树,即每个节点分支数量可能为多个,根节点不包含字符串。
从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
除根节点外,每一个节点只包含一个字符。
每个节点的所有子节点包含的字符都不相同。
优点:利用字符串公共前缀来减少查询时间,减少无谓的字符串比较
Trie 树图示:
(为 b,abc,abd,bcd,abcd,efg,hii 这7个单词创建的trie树, https://baike.baidu.com/item/字典树/9825209)
trie 树的代码实现:https://baike.baidu.com/item/字典树/9825209#5
Radix Tree基数树
认识基数树:
Radix Tree,基数特里树或压缩前缀树,是一种更节省空间的 Trie 树。它对 trie 树进行了压缩。
看看是咋压缩的,假如有下面一组数据 key-val 集合:
Copy{
"def": "redisio",
"dcig":"mysqlio",
"dfo":"linux",
"dfks":"tdb",
"dfkz":"dogdb",
}
用上面数据中的 key 构造一颗 trie 树:
现在压缩 trie 树(Compressed Trie Tree)中的唯一子节点,就可以构建一颗 radix tree 基数树。
父节点下第一级子节点数小于 2 的都可以进行压缩,把子节点合并到父节点上,把上图 <2 子节点数压缩,变成如下图:
把 c,f 和 c,i,g 压缩在一起,这样就节省了一些空间。压缩之后,分支高度也降低了。
这个就是对 trie tree 进行压缩变成 radix tree。
在另外看一张出现次数比较多的 Radix Tree 的图:
(图Radix_tree 来自:https://en.wikipedia.org/wiki/Radix_tree)
基数树唯一子节点都与其父节点合并,边沿(edges)既可以存储多个元素序列也可以存储单个元素。比如上图的 r, om,an,e。
基数树的图最下面的数字对应上图的排序数字,比如 ,就是 ruber 字符,。
什么时候使用基数树合适:
字符串元素个数不是很多,且有很多相同前缀时适合使用基数树这种数据结构。
基数树的应用场景:
httprouter 中的路由器。
使用 radix tree 来构建 key 为字符串的关联数组。
很多构建 IP 路由也用到了 radix tree,比如 linux 中,因为 ip 通常有大量相同前缀。
Redis 集群模式下存储 slot 对应的所有 key 信息,也用到了 radix tree。文件 rax.h/rax.c 。
radix tree 在倒排索引方面使用也比较广。
httprouter中的基数树
Copy// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L46
type node struct {path string // 节点对应的字符串路径wildChild bool // 是否为参数节点,如果是参数节点,那么 wildChild=truenType nodeType // 节点类型,有几个枚举值可以看下面nodeType的定义maxParams uint8 // 节点路径最大参数个数priority uint32 // 节点权重,子节点的handler总数indices string // 节点与子节点的分裂的第一个字符children []*node // 子节点handle Handle // http请求处理方法
}
节点类型 nodeType 定义:
Copy// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L39
// 节点类型
const (static nodeType = iota // default, 静态节点,普通匹配(/user)root // 根节点param // 参数节点(/user/:id)catchAll // 通用匹配,匹配任意参数(*user)
)
indices 这个字段是缓存下一子节点的第一个字符。
比如路由: r.GET("/user/one"), r.GET("/user/two"), indices 字段缓存的就是下一节点的第一个字符,即 “ot” 2个字符。这个就是对搜索匹配进行了优化。
如果 wildChild=true,参数节点时,indices=""。
addRoute 添加路由:
addRoute(),添加路由函数,这个函数代码比较多,
分为空树和非空树时的插入。
空树时直接插入:
Copyn.insertChild(numParams, path, fullPath, handlers)
n.nType = root // 节点 nType 是 root 类型
非空树的处理:
先是判断树非空(non-empty tree),接着下面是一个 for 循环,下面所有的处理都在 for 循环面。
- 更新 maxParams 字段
- 寻找共同的最长前缀字符
Copy// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L100
// Find the longest common prefix. 寻找字符相同前缀,用 i 数字表示
// This also implies that the common prefix contains no ':' or '*',表示没有包含特殊匹配 : 或 *
// since the existing key can't contain those chars.
i := 0
max := min(len(path), len(n.path))
for i < max && path[i] == n.path[i] {i++
}
- split edge 开始分裂节点
比如第一个路由 path 是 user,新增一个路由 uber,u 就是它们共同的部分(common prefix),那么就把 u 作为父节点,剩下的 ser,ber 作为它的子节点
Copy// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L107
// Split edge
if i < len(n.path) {child := node{path: n.path[i:], // 上面已经判断了匹配的字符共同部分用i表示,[i:] 从i开始计算取字符剩下不同部分作为子节点wildChild: n.wildChild, // 节点类型nType: static, // 静态节点普通匹配indices: n.indices,children: n.children,handle: n.handle,priority: n.priority - 1, // 节点降级}// Update maxParams (max of all children)for i := range child.children {if child.children[i].maxParams > child.maxParams {child.maxParams = child.children[i].maxParams}}n.children = []*node{&child} // 当前节点的子节点修改为上面刚刚分裂的节点// []byte for proper unicode char conversion, see #65n.indices = string([]byte{n.path[i]})n.path = path[:i]n.handle = niln.wildChild = false
}
- ihttps://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L137
- 4.1 n.wildChild = true,对特殊参数节点的处理 ,: 和 *
Copy // https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L133if n.wildChild {n = n.children[0]n.priority++// Update maxParams of the child nodeif numParams > n.maxParams {n.maxParams = numParams}numParams--// Check if the wildcard matchesif len(path) >= len(n.path) && n.path == path[:len(n.path)] &&// Adding a child to a catchAll is not possiblen.nType != catchAll &&// Check for longer wildcard, e.g. :name and :names(len(n.path) >= len(path) || path[len(n.path)] == '/') {continue walk} else {// Wildcard conflictvar pathSeg string... ...}}
- 4.2 开始处理 indices
Copy// https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go#L171
c := path[0] // 获取第一个字符// slash after param,处理nType为参数的情况
if n.nType == param && c == '/' && len(n.children) == 1// Check if a child with the next path byte exists
// 判断子节点是否和当前path匹配,用indices字段来判断
// 比如 u 的子节点为 ser 和 ber,indices 为 u,如果新插入路由 ubb,那么就与子节点 ber 有共同部分 b,继续分裂 ber 节点
for i := 0; i < len(n.indices); i++ {if c == n.indices[i] {i = n.incrementChildPrio(i)n = n.children[i]continue walk}
}// Otherwise insert it
// indices 不是参数和通配匹配
if c != ':' && c != '*' {// []byte for proper unicode char conversion, see #65n.indices += string([]byte{c})child := &node{maxParams: numParams,}// 新增子节点n.children = append(n.children, child)n.incrementChildPrio(len(n.indices) - 1)n = child
}
n.insertChild(numParams, path, fullPath, handle)
- i=len(path)路径相同
如果已经有handler处理函数就报错,没有就赋值handler
insertChild 插入子节点:
insertChild
getValue 路径查找:
getValue
上面2个函数可以独自分析下 - -!
可视化radix tree操作
https://www.cs.usfca.edu/~galles/visualization/RadixTree.html
radix tree 的算法操作可以看这里,动态展示。
参考
- https://github.com/gin-gonic/gin/tree/v1.7.0 gin 源码
- https://github.com/julienschmidt/httprouter httprouter地址
- https://github.com/julienschmidt/httprouter/blob/v1.3.0/tree.go httprouter tree源码
- https://gin-gonic.com/docs/quickstart/ gin doc
- https://www.cs.usfca.edu/~galles/visualization/RadixTree.html radix tree算法步骤可视化
- https://baike.baidu.com/item/字典树/9825209 百科基数树
- 《算法》 5.2 单词查找树 trie tree 作者: Robert Sedgewick / Kevin Wayne
- https://en.wikipedia.org/wiki/Trie 维基trie树(en)
- https://en.wikipedia.org/wiki/Radix_tree 维基radix tree(en)
- https://zh.wikipedia.org/wiki/基数树 维基基数树(zh)
- https://github.com/redis/redis/blob/6.0.14/src/rax.c redis 中 radix tree 使用
- https://github.com/redis/redis/blob/6.0.14/src/rax.h redis 中 radix tree 使用
一步一步分析Gin框架路由源码及radix tree基数树相关推荐
- Apollo 2.0 框架及源码分析(一) | 软硬件框架
原文地址:https://zhuanlan.zhihu.com/p/33059132 前言 如引言中介绍的,这篇软硬件框架多为现有消息的整合加一些个人的想法.关于 Apollo 介绍的文章已经有许多, ...
- 【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )
Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...
- skynet 框架snax源码分析----变量注入
skynet为了简化服务的编写,推出了snax框架,源码里也有一个例子pingserver.这是snax原创文章的第一篇,所以先就分析snax框架里的interface.lua源码,它的实现应用了一个 ...
- 视频教程-RPC服务框架(Dubbo)源码分析-Java
RPC服务框架(Dubbo)源码分析 鲁班学院-子路老师曾就职于谷歌.天猫电商等多家互联网公司,历任java架构师.研发经理等职位,参与并主导千万级并发电商网站与后端供应链研发体系搭建,多次参与电商大 ...
- Java类集框架 —— LinkedHashMap源码分析
前言 我们知道HashMap底层是采用数组+单向线性链表/红黑树来实现的,HashMap在扩容或者链表与红黑树转换过程时可能会改变元素的位置和顺序.如果需要保存元素存入或访问的先后顺序,那就需要采用L ...
- 如何理解 Flutter 路由源码设计?| 开发者说·DTalk
本文原作者: Nayuta,原文发布于: 进击的 Flutter 本期看点: 70 行代码实现一个丐版路由 路由源码细节解析 导语 某天在公众号看到这样一个问题 这问题我熟啊,刚好翻译 Overl ...
- [并发编程] - Executor框架#ThreadPoolExecutor源码解读01
文章目录 Pre Thread Java线程与OS线程 生命状态 状态切换 线程池 why use case Advantage Executor框架 ThreadPoolExecutor 源码分析 ...
- Java熔断框架有哪些_降级熔断框架 Hystrix 源码解析:滑动窗口统计
降级熔断框架 Hystrix 源码解析:滑动窗口统计 概述 Hystrix 是一个开源的降级熔断框架,用于提高服务可靠性,适用于依赖大量外部服务的业务系统.什么是降级熔断呢? 降级 业务降级,是指牺牲 ...
- ThinkPHP路由源码解析(三)
本文接着上文继续来解读路由源码,如果你看到本文可以先看一下之前写的路由文章,共计俩篇. ThinkPHP路由源码解析 前言 一.检测路由-合并分组参数.检查分组路由 二.检测URL变量和规则路由是否匹 ...
- Flutter 路由源码解析
前言 这一次,我尝试以不贴一行源代码的方式向你介绍 Flutter 路由的实现原理,同时为了提高你阅读源码的积极性,除了原理介绍以外,又补充了两个新的模块:从源码中学习到的编程技巧,以及 阅读源码之后 ...
最新文章
- PCL :K-d tree 2 结构理解
- 继续说一下2016里面的json功能(1)
- 慢动作输出 Linux 命令结果并用彩色显示
- Spring Boot怎么样注册Servlet三大组件[Servlet、Filter、Listener]
- 2.内核异常处理流程
- kibana安装与Kibana server is not ready yet
- 白左机器人_乔治高中 - George School | FindingSchool
- BZOJ 3083: 遥远的国度(树链剖分+DFS序)
- sql判断时间差值_Oracle判断某人员在某地是否有超过指定时间的停留
- RocketMQ的一些基本概念和RocketMQ特性的讲解
- set 有序吗js_2021了,你的vue实践够熟练了吗?源码思维呢?
- 浙大 PAT 甲级1009
- 一文看懂深度学习新王者「AutoML」:是什么、怎么用、未来如何发展?
- “音”你而来,“视”而可见 腾讯云+社区音视频技术开发实战沙龙圆满结束...
- Warning: Each record in table should have a unique `key` prop,or set `rowKey` to 解决方法
- set python_set在python里的含义和用法
- 达观杯文本分类——基于N-gram和LogisticRegression
- java 坦克大战暂停_java实现坦克大战游戏
- php 模拟登陆微信,微信公众平台模拟登陆有关问题
- PC电脑使用无线网卡连接上手机热点,为什么不能上网