Go第三方框架--gin框架(二)

4. gin框架源码–Engine引擎和压缩前缀树的建立

讲了这么多 到标题4才开始介绍源码,主要原因还是想先在头脑中构建起 一个大体的框架 然后再填肉 这样不容易得脑血栓。标题四主要涉及标题2.3的步骤一
也就是 标题2.3中的 粗线框中的内容

4.1 Engine 引擎的建立

见 TestGin的第 一行 我们按图索骥 一步步深入看看 只需关注 我中文注释的属性或代码就行

	r := gin.Default() // 建立一个默认的 gin 引擎实例

其中函数 Default() 代码 如下:

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()  // 打印 警告信息
	engine := New()  // 建立 engine 实例
	engine.Use(Logger(), Recovery()) // 添加中间件 函数 包括 日志和panic恢复函数
	return engine
}

其中 New()函数 代码如下(只关注我中文解释的参数就行):

// New 返回一个不带任何中间件函数的新的engine实例
func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{        // 建立默认根路由组 也就是说 所有的 路由 都需要经过本 在前面加上 “/”  后续路由(使用Group(...)函数创建) 会在其基础上 不断叠加更新 basePath和Handlers 但其功能还是一样的
			Handlers: nil,
			basePath: "/",
			root:     true,
		},
		FuncMap:                template.FuncMap{},
		RedirectTrailingSlash:  true,
		RedirectFixedPath:      false,
		HandleMethodNotAllowed: false,
		ForwardedByClientIP:    true,
		RemoteIPHeaders:        []string{"X-Forwarded-For", "X-Real-IP"},
		TrustedPlatform:        defaultPlatform,
		UseRawPath:             false,
		RemoveExtraSlash:       false,
		UnescapePathValues:     true,
		MaxMultipartMemory:     defaultMultipartMemory
		trees:                  make(methodTrees, 0, 9),   // 初始化 方法树的 列表 包括 Get/Post/Delete等九中方法
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJSONPrefix:       "while(1);",
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
		trustedCIDRs:           defaultTrustedCIDRs,
	}
	engine.RouterGroup.engine = engine   // 将新建的 本 engine 索引付给 engine.RouterGroup.engine 方便 路由组引用 本engine的 addRoute()方法 
	engine.pool.New = func() any {                // 初始化context池 因为涉及到大量客户端连接,所以将context池化 减少对象的创建和回收次数 可以节省内存 减少垃圾回收时间 提高效率
		return engine.allocateContext(engine.maxParams)
	}
	return engine
}

其中 engine.Use(Logger(), Recovery()) 中 Use()函数 如下

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)  // 将中间件函数 加入到 RouterGroup的参数 Handlers 列表中 作为本路由组的整体的中间函数  
	engine.rebuild404Handlers() 
	engine.rebuild405Handlers()
	return engine
}

到这里 r := gin.Default() 这行代码就讲解完毕了 比较粗略 因为这部分源码不太难 主要是 完成了两件事
1: 创建了一个默认路由组,主要来存放根 路径 “/” 和 全局中间函数 包括 对日志和panic的处理
2: 初始化了一个 methodTrees 结构体 用来 保存 get/post等9种方法树的根节点 还有其他初始参数 跟本文讲解的内容关系不大 不做讲解 感兴趣的可以自己谷歌上百度两下。
到这里 默认引擎就建立起来了 。

4.2 GET方法 路由节点树的逐步建立
4.2.1 树初始化 和 第一个路由节点的建立

见 TestGin的第 二行,代码如下

	r.GET("/aa/bb", func(c *gin.Context) { c.JSON(200, gin.H{"route path ": "/aa/bb"}) })

第一次建立路由树时,树是空的,所以需要经过判断建立根节点和直接将路由和函数 挂到树上。
树建立后的结构如下(handler也就是 注册的函数 在路径插入时插入,故不在图中展示,只在有特殊情况时说明。):
在这里插入图片描述
知道了结果,现在我们来梳理下代码:
从 r.Get 追踪下来 可见如图代码


func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)   // 组的根路径(/)+用户路径(/aa/bb)
	handlers = group.combineHandlers(handlers)  // 组的根方法组(日志相关+panic相关)+用户注册方法(c.Json......)
	group.engine.addRoute(httpMethod, absolutePath, handlers)   // 构造路由树核心函数 将 absolutePath 和 handler 添加到 httpMathod 方法(Get)对应的 路由树上
	return group.returnObj()   // 没看懂 不过不影响 哦 不对 主逻辑 ps: 不是能力不行 写这篇代码时 有点困 没深究呢
}

接着追踪 group.engine.addRoute(httpMethod, absolutePath, handlers)

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
 	// 隐藏无关代码
 		
	root := engine.trees.get(method)
	if root == nil {       // 第一次 建立路由 会走这里 主要是 初始化 路由树的 根节点,将根节点挂载到 engine的 trees 列表中
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
	root.addRoute(path, handlers) // 构造路由树核心函数,将 路径(/aa/b) 和 方法(3个) 添加到路由树上 根节点是(root)

		// 隐藏无关代码
}

接着追踪 root.addRoute(path, handlers)

func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++  // priority 可以理解 以n为根节点的孩子节点的总个数 主要作用是 用来对 其第一层孩子节点进行重排,按照值大小倒序排列。为什么这么做呢,因为孩子节点多,说明通过这个节点的路由就多 访问就越频繁 在for循环寻找路由时 排在前面的经常被访问的节点 可以快速找到 从而可以提高效率。后续会讲解 如果有疑惑 先放着

	// Empty tree
	if len(n.path) == 0 && len(n.children) == 0 {  // n 是上面代码中的 root ,可以看到 只有 fullPath有值,path和children都是 零值(其含义见3.3),
		n.insertChild(path, fullPath, handlers)   // 为root根节点补充完整参数。因为其没有孩子节点 path也为空 则可以判断是空树 所以 root节点就是第一个节点 只需补充完整其参数就行
		n.nType = root
		return
	}

	parentFullPathIndex := 0
	// 以下代码是 gin框架构造路由树核心,只是初始构造路由树用不到,先排除干扰
}

接着追踪 n.insertChild(path, fullPath, handlers)

func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
	for {
		// Find prefix until first wildcard
		wildcard, i, valid := findWildcard(path)  // 寻找通配符 本文暂时只介绍 gin框架 精髓 无通配符路由节点建立  
		if i < 0 { // No wildcard found  //i==-1 break
			break
		}

		// 隐藏无关代码
		}

	// If no wildcard was found, simply insert the path and handle 没有通配符 简单的插入 路径和handler
	n.path = path // (/aa/bb)
	n.handlers = handlers  // 3个  (日志相关+panic相关)+用户注册方法(c.Json......)
	n.fullPath = fullPath // (/aa/bb)
}
		

至此 第一个 节点便建立起来了,TestGin的第 二行执行完毕后,其engine结构如下图,可以看到root节点确实如分析的一般。
在这里插入图片描述

4.2.2 第2个路由路径插入

代码 如图
在这里插入图片描述
这行代码执行完毕后得到的树如图:
在这里插入图片描述
下面我们来具体分析下代码
通过代码追踪 可以看到 第二个节点建立时 跳过了 root节点初始化和 第一个节点建立的代码,来到了 addRoute函数的核心部分
接着追踪 root.addRoute(path, handlers)

func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++   
	// 隐藏无关代码
	
	parentFullPathIndex := 0

walk:
	for {
		// Find the longest common prefix.
		// This also implies that the common prefix contains no ':' or '*'
		// since the existing key can't contain those chars.
		i := longestCommonPrefix(path, n.path)   // n是4.2.1形成的第一个节点 则 n.path =/aa/bb path==/aa/bd longestCommonPrefix 函数是寻找两个字符串第一个不相同的字符的 索引 这里可以算出来 是 5。
		                                         // 这个函数的作用决定父节点是否需要被拆分, 当n.path是path的子节点时 不被拆分,否则就需要被拆分。

		// Split edge
		if i < len(n.path) {   // 5<6 走这里 ,拆分父节点为两部分 /aa/b(根节点) 和 b(第一个孩子节点)  ,下面具体讲解
			child := node{     //  b 孩子节点的创建 
				path:      n.path[i:], // 将 b赋值给 path
				wildChild: n.wildChild,
				nType:     static,
				indices:   n.indices,    // child继承了 n的主要参数和功能
				children:  n.children,  // 将其孩子节点赋值给 其 分裂的前缀节点
				handlers:  n.handlers,
				priority:  n.priority - 1, // 没看懂
				fullPath:  n.fullPath, // 全路径不变 还是/aa/bb
			}

			n.children = []*node{&child}  // 初始化n节点,并且将 child加入到n的孩子节点中,这样 就形成了 n-->n.children-->n.children.children这样三层节点结构
			// []byte for proper unicode char conversion, see #65
			n.indices = bytesconv.BytesToString([]byte{n.path[i]})  // 下面是 重新给n的属性赋值
			n.path = path[:i]
			n.handlers = nil
			n.wildChild = false
			n.fullPath = fullPath[:parentFullPathIndex+i]
		}

		// Make new node a child of this node   // 将n 的第二个节点d插入进去
		if i < len(path) {  // 5<6 将 新节点 d插入进去
			path = path[i:]  //新节点的 path参数是是 d
			c := path[0] // 获取path第一个 字符,用于快速定位应该从哪一个节点向下匹配。

			// '/' after param

			// todo 没看懂
			if n.nType == param && c == '/' && len(n.children) == 1 {
			// 隐藏无关代码
			}

			// Otherwise insert it
			if c != ':' && c != '*' && n.nType != catchAll {
				// []byte for proper unicode char conversion, see #65
				n.indices += bytesconv.BytesToString([]byte{c})  // 将 n的新孩子节点前缀字符加入到 indices 中
				child := &node{
					fullPath: fullPath,
				}
				n.addChild(child)  // 将孩子节点添加到 n的孩子节点数组中
				n.incrementChildPrio(len(n.indices) - 1) // 这里比较重要 用到了我们所说的 priority, 按照 其值 大小进行子节点倒序排序。原因前文讲过了,请自行查看。
				n = child  // 将 child地址 赋值给 n。
			} else if n.wildChild {
			// 这里是对通配符的处理,本文暂不涉及,略过。
			}

			n.insertChild(path, fullPath, handlers)  // 完善n(child)的 path,fullPath,handlers ,insertChild函数其实就是给节点n的几个属性赋值,所以感觉起得名字有点误导。
			return  // 返回 到这里 第二个节点就创建好了 
		}

		// Otherwise add handle to current node
		if n.handlers != nil {
			panic("handlers are already registered for path '" + fullPath + "'")
		}  // 如果i>=len(path) 就可能是情况 n.path==/aa/bb path==/aa/b 。则分类后 一共 两个节点 没有 d节点。 则需要将 handler和fullPath 赋值当前n节点。
		n.handlers = handlers
		n.fullPath = fullPath
		return  返回
	}
}

到这里第二个路由节点就插入进去了。
我们看下 debugger的结果
在这里插入图片描述
可以看到跟我们分析的是一致的。

4.2.3 第3-5个路由路径的插入

由于其 执行的动作跟 4.2.2一致,所以略过。其构建树的过程如下图
在这里插入图片描述

4.2.4 第6个路由的插入

4.2.1–4.2.3是正常路由的插入,一次循环就能完成。没有涉及到 addRoute() 函数的关键字 walk: 接下来 我们再引入一个路由 来触发 for 循环,添加的路由如下图
在这里插入图片描述
这行代码执行完毕后得到的树如图:
在这里插入图片描述
下面 我们来分析下具体的代码执行 还是直接 分析 root.addRoute(path, handlers)
第1层循环

func (n *node) addRoute(path string, handlers HandlersChain) {
	fullPath := path
	n.priority++

	// Empty tree
	// 隐藏无关代码

	parentFullPathIndex := 0
// 循环第一层 沿着 树节点 向下找 则n节点 是4.2.3最右边的图  以下 n的参数都可以参考这个图来获取
walk:
	for {
		// Find the longest common prefix.
		// This also implies that the common prefix contains no ':' or '*'
		// since the existing key can't contain those chars.
		i := longestCommonPrefix(path, n.path)  // n.path == "/" path=="/aa/be" 如此i==1

		// Split edge
		if i < len(n.path) {  // 不符合 跳过
			// 隐藏无关代码
		}

		// Make new node a child of this node
		if i < len(path) {  // 符合
			path = path[i:] // 获取 除了根节点(“/”)的 剩余部分 aa/be
			c := path[0]   // 将 aa/be 的 第一个字符 a 提取出来 为了 快速在 n的孩子节点定位和将c插入n的 indices中(如果没有匹配n的子节点)

			// '/' after param
 
			if n.nType == param && c == '/' && len(n.children) == 1 {
					// 隐藏无关代码
			}

			// Check if a child with the next path byte exists
			for i, max := 0, len(n.indices); i < max; i++ {  // n.indices=={a,e}
				if c == n.indices[i] {        // 因为c==rune("a") 所以 走下面代码
					parentFullPathIndex += len(n.path)  // 下一个循环 中 节点的 fullPath在 全局全路径 中出现的索引。
					i = n.incrementChildPrio(i)  // 因为接下来的 新节点 要加入到  包含 n.children[i]节点的后续子节点中 所以其 孩子几点数(priority) 要增1,然后返回排序后的 原始的n.children[i]节点的新的索引 给 i
					n = n.children[i] // 将孩子节点赋值给 n 
					continue walk  // 从 新节点开始继续循环 见  4.2.3最右边节点的第2层最左边节点
				}
			}

			// 隐藏无关代码
	}
}

第2-3层循环跟上面代码 原理相同 可参考 4.2.3最右边节点的第2-3层
第4层循环时 n.path==“b” path==“be”
其 执行逻辑可以按照 4.2.3执行 请自行验证
至此 树节点的建立就梳理完毕了,注意只是梳理了不带通配符的路由处理逻辑,关于通配符 例如 :* 等特殊字符请自行梳理。

梳理了这么多知识点 都是属于 1.3 的圆圈1,下面我们改建立起tcp监听了。

5. net/http框架源码-- 多路复用的实现

这块核心功能对应 1.3 的圆圈2,所属代码如下图:
在这里插入图片描述
未完待续…

6. gin框架源码–路由匹配(压缩前缀树的查找)

7. 收尾

ps: 本人菜鸟 不太专业 如果有错还请各位大侠指出;免责声明:凡是按照本八股文去面试被怼的,本人概不承担责任;面试通过,请自行心理默念几遍博主最帅。
参考文章
https://juejin.cn/post/7263826380889915453

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/489116.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

R语言学习——Rstudio软件

R语言免费但有点难上手&#xff0c;是数据挖掘的入门级别语言&#xff0c;拥有顶级的可视化功能。 优点&#xff1a; 1统计分析&#xff08;可以实现各种分析方法&#xff09;和计算&#xff08;有很多函数&#xff09; 2强大的绘图功能 3扩展包多&#xff0c;适合领域多 …

【Python实用标准库】argparser使用教程

argparser使用教程 1.介绍2.基本使用3.add_argument() 参数设置4.参考 1.介绍 &#xff08;一&#xff09;argparse 模块是 Python 内置的用于命令项选项与参数解析的模块&#xff0c;其用主要在两个方面&#xff1a; 一方面在python文件中可以将算法参数集中放到一起&#x…

基于nodejs+vue基于协同过滤算法的私人诊python-flask-django-php

实现后的私人诊所管理系统基于用户需求分析搭建的&#xff0c;并且会有个人中心&#xff0c;患者管理&#xff0c;医生管理&#xff0c;科室管理&#xff0c;出诊医生管理&#xff0c;预约挂号管理&#xff0c;预约取消管理&#xff0c;病历信息管理&#xff0c;药品信息管理&a…

国内IP节点更换攻略,一键解决烦恼

在如今的互联网时代&#xff0c;网络已经成为人们生活中不可或缺的一部分。而对于许多网民来说&#xff0c;网络速度的快慢直接关系到他们的上网体验。在国内&#xff0c;很多用户常常遇到一个问题&#xff0c;那就是网络速度不稳定。有时候可以流畅地上网&#xff0c;有时候却…

【LeetCode热题100】108. 将有序数组转换为二叉搜索树(二叉树)

一.题目要求 给你一个整数数组 nums &#xff0c;其中元素已经按 升序 排列&#xff0c;请你将其转换为一棵 平衡二叉搜索树。 二.题目难度 简单 三.输入样例 示例 1&#xff1a; 输入&#xff1a;nums [-10,-3,0,5,9] 输出&#xff1a;[0,-3,9,-10,null,5] 解释&#x…

React Native 应用打包上架

引言 在将React Native应用上架至App Store时&#xff0c;除了通常的上架流程外&#xff0c;还需考虑一些额外的优化策略。本文将介绍如何通过配置App Transport Security、Release Scheme和启动屏优化技巧来提升React Native应用的上架质量和用户体验。 配置 App Transport…

【目标检测】西红柿成熟度数据集三类标签原始数据集280张

文末有分享链接 标签名称names: - unripe - semi-ripe - fully-ripe D00399-西红柿成熟度数据集三类标签原始数据集280张

Spring文件配置以及获取

前言 我们都知道很多应用都是有配置文件的,可以对应用的一些参数进行配置,如conf... 本篇我们讲解关于Spring的配置文件以及程序怎么获取其中写入的参数 Spring中的配置文件主要有三种 还有yml和ymal文件 下面我们将介绍关于常用的两种 preoperties 和 yml文件的格式和读取…

【SQL】1667. 修复表中的名字(UPPER()、LOWER()、SUBSTRING()、CONCAT())

前述 SQL中字符串截取函数(SUBSTRING) SQL 字母大小写转换函数UPPER()、UCASE()、LOWER()和LCASE() 题目描述 leetcode题目&#xff1a;1667. 修复表中的名字 Code select user_id, concat(upper(substring(name, 1, 1)),lower(substring(name, 2)) ) as name from Users o…

5G双域专网+零信任的神奇魔法

引言 在当今数字化程度不断提升的社会中&#xff0c;信息安全已经成为企业和组织发展的关键要素之一。特别是在网络连接日益广泛的环境下&#xff0c;对于数据的保护和隐私的维护变得尤为重要。随着5G技术的飞速发展&#xff0c;5G双域专网为企业提供了更快速、更可靠的连接&a…

对标开源3D建模软件blender,基于web提供元宇宙3D建模能力的dtns.network德塔世界是否更胜一筹?

对标开源3D建模软件blender&#xff0c;基于web提供元宇宙3D建模能力的dtns.network德塔世界是否更胜一筹&#xff1f; blender是一款优秀的3D建模开源软件&#xff0c;拥有免费开源、功能强大、渲染速度优秀的优点。而开源的dtns.network德塔世界&#xff0c;亦是专业级的元宇…

cinder学习小结

1 官方文档 翻译官方文档学习 链接Cinder Administration — cinder 22.1.0.dev97 documentation (openstack.org) 1.1 镜像压缩加速 在cinder.conf配allow_compression_on_image_upload True可打开开关 compression_format xxx可设置镜像压缩格式&#xff0c;可为gzip 1.2 …

【MATLAB源码-第11期】基于matlab的2FSK的误码率BER仿真以及原信号调制信号解调信号波形展示。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 2FSK&#xff08;Frequency Shift Keying&#xff09;为二进制数字频率调制&#xff08;二进制频移键控&#xff09;&#xff0c;用载波的频率来传送数字信息&#xff0c;即用所传送的数字信息控制载波的频率。2FSK信号便是符…

vulnhub-----pWnOS1.0靶机

文章目录 1.信息收集2.漏洞测试3.爆破hash4.提权 首先拿到一台靶机&#xff0c;就需要知道靶机的各种信息&#xff08;IP地址&#xff0c;开放端口&#xff0c;有哪些目录&#xff0c;什么框架&#xff0c;cms是什么&#xff0c;网页有什么常见的漏洞&#xff0c;如sql注入&…

如何添加随机种子保证代码每次复现的一致性?

如何添加随机种子保证代码每次复现的一致性&#xff1f; 在main()程序中首先设定随机种子&#xff1a; def set_seed(seed42):os.environ[PYTHONHASHSEED] str(seed)random.seed(seed)np.random.seed(seed)torch.manual_seed(seed)torch.cuda.manual_seed(seed)torch.backends…

【文本挖掘与文本分析】上机实验三

实验目的和要求 实验 了解sklearn,gensim可视化主题的基本操作&#xff1b;采集四大名著之《红楼梦》进行主题分析对《红楼梦》的主题进行可视化 或采集二十大报告进行主题分析&#xff1b;对《二十大报告》的主题进行可视化 数据来源 《红楼梦》小说《二十大报告》 采集红…

ubuntu下安装minconda

1.搜索清华源 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 2.搜索conda 3.选一个合适自己的下载到本地 4.将下载的文件传入到ubuntu中 bash Miniconda3-py311_23.11.0-1-Linux-x86_64.sh 安装 5.source ~/.bashrc 激活即可&#xff08;必要步骤&#xff09;

2024年 导出环境依赖requirements.txt

2024年 导出环境依赖 一、前言 有时候需要导出环境依赖&#xff0c;遂记录一下这个短短的步骤 二、具体步骤 1、使用pip进行安装和管理环境 安装导出依赖的库pipreqs pip install pipreqs将环境依赖项导出到当前目录的requirements.txt文件&#xff0c;编码格式用utf-8 …

第15篇:2位数值比较器

Q&#xff1a;本篇我们将实现2位二进制数值比较器逻辑电路&#xff0c;即对2个2位二进制数大小进行比较。 A&#xff1a;数值比较器的基本原理&#xff1a;先对两个数的高位进行比较&#xff0c;若高位相等&#xff0c;再比较低位&#xff0c;低位的比较结果决定两个数的大小。…

自增不再简单:深入探索MySQL自增ID的持久化之道

概述 MySQL中的自增特性估计大家或多或少都是用过。一张表中只能由一个自增字段&#xff0c;通常我们会把它设置为主键&#xff0c;但是随着大家系统越来越分布式&#xff0c;为了一些性能和可扩展性问题&#xff0c;大家目前选择更多的都是分布式ID&#xff08;雪花算法、UUI…