Gin 源码概览 - 路由

本文基于gin 1.1 源码解读
https://github.com/gin-gonic/gin/archive/refs/tags/v1.1.zip

1. 注册路由

我们先来看一段gin代码,来看看最终得到的一颗路由树长啥样

func TestGinDocExp(t *testing.T) {
	engine := gin.Default()
	engine.GET("/api/user", func(context *gin.Context) {
		fmt.Println("api user")
	})
	engine.GET("/api/user/info/a", func(context *gin.Context) {
		fmt.Println("api user info a")
	})
	engine.GET("/api/user/information", func(context *gin.Context) {
		fmt.Println("api user information")
	})
	engine.Run()
}

看起来像是一颗前缀树,我们后面再仔细深入源码

1.1 gin.Default

gin.Default() 返回了一个Engine 结构体指针,同时添加了2个函数,LoggerRecovery

func Default() *Engine {
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

New方法中初始化了 Engine,同时还初始化了一个RouterGroup结构体,并将Engine赋值给RouterGroup,相当于互相套用了

func New() *Engine {
    engine := &Engine{
        RouterGroup: RouterGroup{
            Handlers: nil,  // 业务handle,也可以是中间件
            basePath: "/",  // 根地址
            root:     true, // 根路由
        },
        RedirectTrailingSlash:  true,
        RedirectFixedPath:      false,
        HandleMethodNotAllowed: false,
        ForwardedByClientIP:    true,
        trees:                  make(methodTrees, 0, 9), // 路由树
    }
    engine.RouterGroup.engine = engine

    // 这里使用pool来池化Context,主要目的是减少频繁创建和销毁对象带来的内存分配和垃圾回收的开销
    engine.pool.New = func() interface{} {   
        return engine.allocateContext()
    }
    return engine
}

在执行完gin.Defualt后,gin的内容,里面已经默认初始化了2个handles

1.2 Engine.Get

在Get方法内部,最终都是调用到了group.handle方法,包括其他的POST,DELETE等

  • group.handle
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 获取绝对路径, 将group中的地址和当前地址进行组合
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 将group中的handles(Logger和Recovery)和当前的handles合并
    handlers = group.combineHandlers(handlers)   

    // 核心在这里,将handles添加到路由树中
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    return group.returnObj()
}
  • group.engine.addRoute
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    // 通过遍历trees中的内容,判断是否在这之前,同一个http方法下已经添加过路由
    // trees 是一个[]methodTree  切片,有2个字段,method 表示方法,root 表示当前节点,是一个node结构体
    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)
}
  • root.addRoute(path, handlers)

这里的代码比较多,其实大家可以简单的认为就是将handlepath进行判断,注意这里的path不是一个完整的注册api,而是去掉了公共前缀后的那部分字符串。

func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path               // 存储当前节点的完整路径
    n.priority++                   // 优先级自增1
    numParams := countParams(path) // 统计当前节点的动态参数个数

    // non-empty tree,非空路由树
    if len(n.path) > 0 || len(n.children) > 0 {
        walk:
        for {
            // Update maxParams of the current node
            // 统计当前节点及子节点中最大数量的参数个数
            // maxParams 可以快速判断当前节点及其子树是否能匹配包含一定数量参数的路径,从而加速匹配过程。(GPT)
            if numParams > n.maxParams {
                n.maxParams = numParams
            }

            // 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 := 0
            max := min(len(path), len(n.path))
            for i < max && path[i] == n.path[i] {
                i++
            }

            // Split edge,当前路径和节点路径只有部分重叠,且存在分歧
            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,
                }

                // Update maxParams (max of all children)
                // 路径分裂时,确保当前节点及子节点中是有最大的maxParams
                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 #65
                n.indices = string([]byte{n.path[i]})
                n.path = path[:i]
                n.handlers = nil
                n.wildChild = false
            }

            // Make new node a child of this node
            if i < len(path) {
                path = path[i:] // 获取当前节点中非公共前缀的部分

                // 当前节点是一个动态路径
                // TODO 还需要好好研究一下
                if n.wildChild {
                    n = n.children[0]
                    n.priority++

                    // Update maxParams of the child node
                    if numParams > n.maxParams {
                        n.maxParams = numParams
                    }
                    numParams--

                    // Check if the wildcard matches
                    // 确保新路径的动态部分与已有动态路径不会冲突。
                    if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
                        // check for longer wildcard, e.g. :name and :names
                        if len(n.path) >= len(path) || path[len(n.path)] == '/' {
                            continue walk
                        }
                    }

                    panic("path segment '" + path +
                      "' conflicts with existing wildcard '" + n.path +
                      "' in path '" + fullPath + "'")
                }

                // 获取非公共前缀部分的第一个字符
                c := path[0]

                // slash after param
                // 当前节点是动态参数节点,且最后一个字符时/,同时当前节点还只有一个字节
             if n.nType == param && c == '/' && len(n.children) == 1 {
                n = n.children[0]
                n.priority++
                continue walk
             }

             // Check if a child with the next path byte exists
             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
             if c != ':' && c != '*' {
                // []byte for proper unicode char conversion, see #65
                n.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, handlers)
             return

          } else if i == len(path) { // Make node a (in-path) leaf
             if n.handlers != nil {
                panic("handlers are already registered for path ''" + fullPath + "'")
             }
             n.handlers = handlers
          }
          return
       }
    } else { // Empty tree
       n.insertChild(numParams, path, fullPath, handlers)
       n.nType = root
    }
}

1.3 Engine.Run

func (engine *Engine) Run(addr ...string) (err error) {
	defer func() { debugPrintError(err) }()

    // 处理一下web的监听地址
	address := resolveAddress(addr)
	debugPrint("Listening and serving HTTP on %s\n", address)

    // 最终还是使用http来启动了一个web服务
	err = http.ListenAndServe(address, engine)
	return
}

2. 路由查找

在上一篇文章中介绍了,http 的web部分的实现,http.ListenAndServe(address, engine) 在接收到请求后,最终会调用engineServeHTTP方法

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    // 从context池中获取一个Context
	c := engine.pool.Get().(*Context)  

    // 对Context进行一些初始值操作,比如赋值w和req
	c.writermem.reset(w)
	c.Request = req
	c.reset()

    // 最终进入这个方法来处理请求
	engine.handleHTTPRequest(c)

    // 处理结束后将Conetxt放回池中,供下一次使用
	engine.pool.Put(c)
}

2.1 engine.handleHTTPRequest()

func (engine *Engine) handleHTTPRequest(context *Context) {
	httpMethod := context.Request.Method  // 当前客户端请求的http 方法
	path := context.Request.URL.Path // 查询客户端请求的完整请求地址

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method == httpMethod {
			root := t[i].root
			// Find route in tree
			handlers, params, tsr := root.getValue(path, context.Params)
			if handlers != nil {
				context.handlers = handlers  // 所有的handles 请求对象
				context.Params = params      // 路径参数,例如/api/user/:id , 此时id就是一个路径参数
				context.Next()  			 // 执行所有的handles方法
				context.writermem.WriteHeaderNow()
				return

			}
		}
	}

	// 这里客户端请求的地址没有匹配上,同时检测请求的方法有没有注册,若没有注册过则提供请求方法错误
	if engine.HandleMethodNotAllowed {
		for _, tree := range engine.trees {
			if tree.method != httpMethod {
				if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {
					context.handlers = engine.allNoMethod
					serveError(context, 405, default405Body)
					return
				}
			}
		}
	}
    // 路由地址没有找到
	context.handlers = engine.allNoRoute
	serveError(context, 404, default404Body)
}

2.2 context.Next()

这个方法在注册中间件的使用会使用的较为频繁

func (c *Context) Next() {
    // 初始化的时候index 是 -1
    c.index++
    s := int8(len(c.handlers))
    for ; c.index < s; c.index++ {
        c.handlers[c.index](c)  // 依次执行注册的handles
    }
}

我们来看一段gin 是执行中间件的流程

func TestGinMdls(t *testing.T) {
	engine := gin.Default()
	engine.Use(func(ctx *gin.Context) {
		fmt.Println("请求过来了")
		// 这里可以做一些横向操作,比如处理用户身份,cors等
		ctx.Next()
		fmt.Println("返回响应")
	})
	engine.GET("/index", func(context *gin.Context) {
		fmt.Println("index")
	})
	engine.Run()
}

curl http://127.0.0.1:8080/index

通过响应结果我们可以分析出,请求过来时,先执行了Use中注册的中间件,然后用户调用ctx.Next() 可以执行下一个handle,也就是用户注册的/index方法的handle

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

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

相关文章

Linux网络序列化与反序列化

Linux网络序列化与反序列化 1. 前言 在网络通信中&#xff0c;互相通信的信息不一定都是字符串&#xff0c;往往一些结构化的信息也需要进行通信。理论上&#xff0c;只要服务器和客户端都自定义一个共同的协议&#xff0c;结构化的信息也能实现正常通信。但考虑到不同系统、…

实战经验:使用 Python 的 PyPDF 进行 PDF 操作

文章目录 1. 为什么选择 PyPDF&#xff1f;2. 安装 PyPDF3. PDF 文件的合并与拆分3.1 合并 PDF 文件3.2 拆分 PDF 文件 4. 提取 PDF 文本5. 修改 PDF 元信息6. PDF 加密与解密6.1 加密 PDF6.2 解密 PDF 7. 页面旋转与裁剪7.1 旋转页面7.2 裁剪页面 8. 实战经验总结 PDF 是一种非…

PhyCAGE:符合物理规律的图像到 3D 生成

Paper: Yan H, Zhang M, Li Y, et al. PhyCAGE: Physically Plausible Compositional 3D Asset Generation from a Single Image[J]. arXiv preprint arXiv:2411.18548, 2024. Introduction: https://wolfball.github.io/phycage/ Code: Unreleased PhyCAGE 是一种 image-to-3D…

游戏为什么失败?回顾某平庸游戏

1、上周玩了一个老鼠为主角的游戏&#xff0c;某平台喜1送的&#xff0c; 下载了很久而一直没空玩&#xff0c;大约1G&#xff0c;为了清硬盘空间而玩。 也是为了拔掉心中的一根刺&#xff0c;下载了而老是不玩总感觉不舒服。 2、老鼠造型比较写实&#xff0c;看上去就有些讨…

上位机工作感想-2024年工作总结和来年计划

随着工作年限的增增长&#xff0c;发现自己越来越不喜欢在博客里面写一些掺杂自己感想的东西了&#xff0c;或许是逐渐被工作逼得“成熟”了吧。2024年&#xff0c;学到了很多东西&#xff0c;做了很多项目&#xff0c;也帮别人解决了很多问题&#xff0c;唯独没有涨工资。来这…

Android系统开发(六):从Linux到Android:模块化开发,GKI内核的硬核科普

引言&#xff1a; 今天我们聊聊Android生态中最“硬核”的话题&#xff1a;通用内核镜像&#xff08;GKI&#xff09;与内核模块接口&#xff08;KMI&#xff09;。这是内核碎片化终结者的秘密武器&#xff0c;解决了内核和供应商模块之间无尽的兼容性问题。为什么重要&#x…

5G 核心网 相关概念快速入门

在我们开始阅读3GPP协议来学习5G核心网之前&#xff0c; 不妨来看看我之前整理的PPT&#xff0c;快速学习核心网相关概念&#xff0c; 以及5G转发面PFCP协议的相关核心知识。 涵盖了最精简的核心骨干内容&#xff0c;助你轻松上阵。 讲解目标 3GPP和相关协议 5G核心网架构模…

2025/1/20 学习Vue的第三天

玩性太大了玩得也不开心&#xff0c;天天看电视刷视频。 内心实在空洞。 最近天天看小红书上的外国人&#xff0c;结实外国友人&#xff08;狗头&#xff09;哈哈哈认识了不少人&#xff0c;有埃及的有美国的&#xff0c;还有天天看菲利普吃糖葫芦哈哈哈哈哈一个阳光的德国大男…

虚幻基础1:hello world

能帮到你的话&#xff0c;就给个赞吧 &#x1f618; 文章目录 hello world创建项目创建关卡创建蓝图将蓝图插入关卡中运行 hello world 本文引擎为5.5.1 创建项目 如图 创建后如图。 创建关卡 如图 创建蓝图 如图 选择actor 双击进入蓝图节点 选择事件图表 创…

SAP POC 项目完工进度 - 收入确认方式【工程制造行业】【新准则下工程项目收入确认】

1. SAP POC收入确认基础概念 1.1 定义与原则 SAP POC&#xff08;Percentage of Completion&#xff09;收入确认方式是一种基于项目完工进度来确认收入的方法。其核心原则是根据项目实际完成的工作量或成本投入占预计总工作量或总成本的比例&#xff0c;来确定当期应确认的收…

SparkSQL数据源与数据存储综合实践

文章目录 1. 打开项目2. 查看数据集2.1 查看JSON格式数据2.2 查看CSV格式数据2.3 查看TXT格式数据 3. 添加单元测试依赖4. 创建数据加载与保存对象4.1 创建Spark会话对象4.2 创建加载JSON数据方法4.3 创建加载CSV数据方法4.4 创建加载Text数据方法4.5 创建加载JSON数据扩展方法…

鸿蒙Harmony json转对象(1)

案例1 运行代码如下 上图的运行结果如下: 附加1 Json_msg interface 案例2 import {JSON } from kit.ArkTS; export interface commonRes {status: numberreturnJSON: ESObject;time: string } export interface returnRes {uid: stringuserType: number; }Entry Component …

Maven私服-Nexus3安装与使用

写在前面 安装简单&#xff0c;此博客主要是为了记录下怎么使用&#xff0c;以及一些概念性的东西 安装配置 下载 下载对应版本&#xff08;科学上网&#xff09; https://help.sonatype.com/en/download-archives—repository-manager-3.html 设置端口 /etc/nexus-defaul…

MindAgent:基于大型语言模型的多智能体协作基础设施

2023-09-18 &#xff0c;加州大学洛杉矶分校&#xff08;UCLA&#xff09;、微软研究院、斯坦福大学等机构共同创建的新型基础设施&#xff0c;目的在评估大型语言模型在游戏互动中的规划和协调能力。MindAgent通过CuisineWorld这一新的游戏场景和相关基准&#xff0c;调度多智…

【k8s面试题2025】2、练气初期

在练气初期&#xff0c;灵气还比较稀薄&#xff0c;只能勉强在体内运转几个周天。 文章目录 简述k8s静态pod为 Kubernetes 集群移除新节点&#xff1a;为 K8s 集群添加新节点Kubernetes 中 Pod 的调度流程 简述k8s静态pod 定义 静态Pod是一种特殊类型的Pod&#xff0c;它是由ku…

K8S-Pod资源清单的编写,资源的增删改查,镜像的下载策略

1. Pod资源清单的编写 1.1 Pod运行单个容器的资源清单 ##创建工作目录 mkdir -p /root/manifests/pods && cd /root/manifests/pods vim 01-nginx.yaml ##指定api版本 apiVersion: v1 ##指定资源类型 kind: Pod ##指定元数据 metadata:##指定名称name: myweb ##用户…

编辑器Vim基本模式和指令 --【Linux基础开发工具】

文章目录 一、编辑器Vim 键盘布局二、Linux编辑器-vim使用三、vim的基本概念正常/普通/命令模式(Normal mode)插入模式(Insert mode)末行模式(last line mode) 四、vim的基本操作五、vim正常模式命令集插入模式从插入模式切换为命令模式移动光标删除文字复制替换撤销上一次操作…

深度学习 DAY1:RNN 神经网络及其变体网络(LSTM、GRU)

实验介绍 RNN 网络是一种基础的多层反馈神经网络&#xff0c;该神经网络的节点定向连接成环&#xff0c;其内部状态可以展示动态时序行为。相比于前馈神经网络&#xff0c;该网络内部具有很强的记忆性&#xff0c;它可以利用它内部的记忆来处理任意时序的输入序列&#xff0c;…

svn tag

一般发布版本前&#xff0c;需要在svn上打个tag。步骤如下&#xff1a; 1、空白处右击&#xff0c;选择TortoiseSVN->Branch/tag; 2、填写To path&#xff0c;即tag的路基以及tag命名&#xff08;一般用版本号来命名&#xff09;&#xff1b;填写tag信息&#xff1b;勾选cr…

Astropay之坑

大家可能知道 Astropay 原来在日本也有业务&#xff0c;后来突然有一天业务关掉了&#xff0c;那里面的用户的钱当然也就取不出来了嘛。 我合计那就那么放着呗&#xff0c;等以后你们重返日本的时候我再去取嘛。 嗨&#xff0c;最近收到几个邮件&#xff0c;可把我气笑了。 简…