gin框架路由源码分析
gin框架的路由结构剖析
-
路由是web框架的核心功能,在学习gin路由前,路由是这样的,比如定义了两个路由/user/get, /user/delete
则会构造出拥有三个节点的路由树,根节点是user,两个子节点分别是:get、delete
上述是一种实现路由的方式,且比较直观,容易理解,对url进行切分,比较,时间复杂度是O(2n) -
gin的路由使用了类似前缀树的数据结构,只需要遍历一遍字符串即可,时间复杂度是O(n)
当然,对于一次http请求来说,这点路由寻址优化可以忽略不急 -
engine的常用设置
func main() {
r := gin.Default()
// 下面两种方式2选1即可,推荐使用第二种
//r.TrustedPlatform = "Client-IP" // 设置客户端真实ip的请求头
// 设置受信任的代理
r.SetTrustedProxies([]string{"127.0.0.1"})
// 设置url中的大写自动转小写,..和//自动移除,
r.RedirectFixedPath = true
// 开启请求方法不允许,并且返回状态码405
r.HandleMethodNotAllowed = true
// 设置允许从远程客户端的哪个header头中获取ip(需搭配设置受信任的代理一起使用)
r.RemoteIPHeaders = append(r.RemoteIPHeaders, "Client-IP")
// TrustedPlatform 设置可信任的平台,如果增加了此项配置,那么获取客户端真实ip的时候
// 会优先从请求头中的Real-IP获取,获取到了直接返回,获取不到才会从RemoteIPHeaders中去获取
// 一般不这样设置,推荐从RemoteIPHeaders中获取,当然前提需要设置受信任的代理, 如果不想设置受信任的代理,那么可以直接从TrustedPlatform中直接获取
//r.TrustedPlatform = "Real-IP"
r.GET("/user/:name", routeUse)
r.Run(":8000")
}
路由的种类
-
静态路由:
框架/用户提前生成一个路由表,一般是map结构,key为URL上的path,value为代码执行点(处理函数),
优点:只需要读取map,没有任何开销,速度奇快
缺点:无法正则匹配路由,只能一一对应,模糊匹配的场景无法使用 -
动态路由:
用户定义好路由匹配规则,框架匹配路由时,根据规则动态去规划路由
优点:适应性强,解决了静态路由的缺点
缺点:相比静态路由有开销,具体视算法和路由匹配规则而定
gin框架路由实现原理
gin框架作为一个轻量级的快速框架,采用的是前缀树的方式实现的动态路由
我们以下面代码为例,看看gin是如何实现路由的
func main() {
r := gin.New()
r.GET("/user/:name", routeUse)
r.Run(":8000")
}
func routeUse(ctx *gin.Context) {
ctx.String(200, "ok")
}
上面我们定义了一个路由 /user/:name , 它会精确匹配/user/:name,而不会匹配/user、/user/、/user/abc/a
接下来我们跟着源码去看看gin是如何实现的
初始化框架
r := gin.New()生成了一个engine对象,engine对象是整个框架的核心,也包含对路由的操作和许多成员变量
其中包含了路由要执行的任务链Handlers HandlersChain, 方法树trees methodTrees
定义路由
r.GET("/user/:name", routeUser)定义了一个GET请求,模糊匹配/user/:name
步骤1:
源码 routergroup.go=>group.calculateAbsolutePath()
将相对路径/user/:name join为绝对路径,因为有可能是个路由组,前面还有一些路径,路由组稍后介绍
步骤2:
源码 routergroup.go=>group.combineHandlers()
判断HandlersChain的长度,不能超过大于等于math.MaxInt8 / 2,也就是不能超过31个,
步骤3:
源码:routergroup.go=>group.engine.addRoute()
装入路由存入 engine.trees 变量
步骤4:
返回当前对象,能达到链式操作的目的
访问路由
输入localhost:8080/user/abc, 首先进入net/http和核心库的走一圈,最终到engine的ServeHttp方法,
将ResponseWriter和Request写入到从pool中新申请的Context中,然后调用engine的handleHTTPRequest方法,
将Context传入,循环之前注册的路由树engine.trees,
匹配method部分源码,gin.go=>handleHTTPRequest():
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue //判断请求method是否相等如果不等则continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
//匹配路由的核心算法,如果匹配出来的value为空则直接退出。
...
...
}
匹配路由核心算法源码:tree.go=>getValue():
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
walk: // Outer loop for walking the tree
for {
prefix := n.path
if len(path) > len(prefix) {
if path[:len(prefix)] == prefix {
//前缀匹配
...
switch n.nType {
case param:
...
(*value.params)[i] = Param{
Key: n.path[1:],
Value: val,
}// 匹配出参数
}
}
if path == prefix {
// 完全匹配,无需匹配参数
...
}
}
}
前缀树算法
前缀树的本质就是一颗查找树,有别于普通的查找树,它适用于一些特殊的场合,比如用于字符串的查找,
比如在一个路由的场景中,有1W个路由字符串,每个字符串长度不等,我们可以用数组来存储,查找的时间复杂度是O(N),
也可以用map来存储,查找的时间复杂度是O(1),但是没办法解决动态匹配的问题,如果使用前缀树的时间复杂度是O(logn),
同时也可以解决动态匹配的问题
下图展示了前缀树的原理,有一下6个字符串,如果要查找cat字符串,步骤如下,
- 先拿到c和root的第一个节点a比较,如果不等,在继续和父节点root的第二个节点比较,直到拿到c
- 然后再拿到a和父节点c的第一个节点a比较,结果相等,则继续往下,
- 在拿到字符t和父节点a的第一个节点t比较,结果相等,则完成
同理在路由中,前缀树可以规划成如下,
具体查找方法和上面一致。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)