Go语言gin框架原理

在gin框架中,最关键的就是前缀树,是很重要的。

gin框架本质上是在http包的基础之上,对其的一个二次封装。

这里借鉴一下小徐先生的图,可能当前版本的gin可能内容有所改变,但大致思想还是这样。

gin框架所做的就是提供一个gin.Engine作为对象Handler注入其中,从而实现路由注册匹配,请求处理链路的优化。

一.核心数据结构

1.1 gin.Engine

type Engine struct {// 路由组RouterGroup// ...// context 对象池pool             sync.Pool// 方法路由树trees            methodTrees// ...
}

Engine 为 Gin 中构建的 HTTP Handler,其实现了 net/http 包下 Handler interface 的抽象方法: Handler.ServeHTTP,因此可以作为 Handler 注入到 net/http 的 Server 当中.

Engine包含的核心内容包括:

  • 路由组 RouterGroup
  • Context 对象池 pool:基于 sync.Pool 实现,作为复用 gin.Context 实例的缓冲池.
  • 路由树数组 trees:共有 9 棵路由树,对应于 9 种 http 方法. 路由树基于压缩前缀树实现,

1.2 http方法

这里要知道一点,很多人都只用常见的4种方法,实际上gin框架并不仅仅只能处理这4种。

还可以处理这些方法:

  • PATCH(用于对资源进行部分更新)
  • HEAD(类似于GET,但只返回响应头)
  • OPTIONS(用于获取服务器支持的请求方法等信息)

const (MethodGet     = "GET"MethodHead    = "HEAD"MethodPost    = "POST"MethodPut     = "PUT"MethodPatch   = "PATCH" // RFC 5789MethodDelete  = "DELETE"MethodConnect = "CONNECT"MethodOptions = "OPTIONS"MethodTrace   = "TRACE"
)

1.3 RouterGroup

type RouterGroup struct {Handlers HandlersChainbasePath stringengine *Engineroot bool
}

RouterGroup 是路由组的概念,其中的配置将被从属于该路由组的所有路由复用:

  • Handlers:路由组共同的 handler 处理函数链. 组下的节点将拼接 RouterGroup 的公用 handlers 和自己的 handlers,组成最终使用的 handlers 链
  • basePath:路由组的基础路径. 组下的节点将拼接 RouterGroup 的 basePath 和自己的 path,组成最终使用的 absolutePath
  • engine:指向路由组从属的 Engine
  • root:标识路由组是否位于 Engine 的根节点. 当用户基于 RouterGroup.Group 方法创建子路由组后,该标识为 false

1.4 HandlersChain

type HandlersChain []HandlerFunctype HandlerFunc func(*Context)

HandlersChain 是由多个路由处理函数 HandlerFunc 构成的处理函数链. 在使用的时候,会按照索引的先后顺序依次调用 HandlerFunc.

二.注册和启动流程

2.1 注册流程

下面以创建 gin.Engine 、注册 middleware 和注册 handler 作为主线,进行源码走读和原理解析:

func main() {// 创建一个 gin Engine,本质上是一个 http Handlermux := gin.Default()// 注册中间件mux.Use(myMiddleWare)// 注册一个 path 为 /ping 的处理函数mux.POST("/ping", func(c *gin.Context) {c.JSON(http.StatusOK, "pone")})// ...
}

接下来说一下注册的流程,在gin框架底层到底发生了什么?

  1. 方法调用:gin.Default -> gin.New
  • 创建一个 gin.Engine 实例
  • 创建 Engine 的首个 RouterGroup,对应的处理函数链 Handlers 为 nil,基础路径 basePath 为 "/",root 标识为 true
  • 构造了 9 棵方法路由树,对应于 9 种 http 方法
  • 创建了 gin.Context 的对象池

  1. 注册middleware

通过 Engine.Use 方法可以实现中间件的注册,会将注册的 middlewares 添加到 RouterGroup.Handlers 中. 后续 RouterGroup 下新注册的 handler 都会在前缀中拼上这部分 group 公共的 handlers.

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {engine.RouterGroup.Use(middleware...)// ...return engine
}

可以看到这个Use函数,可以同时注册多个中间件,他们是有一定的执行顺序的。

  1. 注册handler

以 http post 为例,注册 handler 方法调用顺序为 RouterGroup.POST-> RouterGroup.handle,接下来会完成三个步骤:

  • 拼接出待注册方法的完整路径 absolutePath
  • 拼接出代注册方法的完整处理函数链 handlers
  • 以 absolutePath 和 handlers 组成 kv 对添加到路由树中

一开始root的路径就是/,如果后续你添加路由组,也就是Group的时候,就会根据拼接,然后对应到相应的路由树去。

2.2 启动流程

这里是gin框架的运行http为主线,看下它的运行流程

func main() {// 创建一个 gin Engine,本质上是一个 http Handlermux := gin.Default()// 一键启动 http 服务if err := mux.Run(); err != nil{panic(err)}
}
func (engine *Engine) Run(addr ...string) (err error) {// ...err = http.ListenAndServe(address, engine.Handler())return
}

Run函数下调用ListenAndServe,传入的是gin框架实现的处理器,从而实现连接

顺便多提一嘴,ListenerAndServe 方法本身会基于主动轮询 + IO 多路复用的方式运行,因此程序在正常运行时,会始终阻塞于 Engine.Run 方法,不会返回.

func (srv *Server) Serve(l net.Listener) error {// ...ctx := context.WithValue(baseCtx, ServerContextKey, srv)for {rw, err := l.Accept()// ...connCtx := ctx// ...c := srv.newConn(rw)// ...go c.serve(connCtx)}
}

通过这个阻塞操作,每当有http请求来时,就会对他处理,然后开启一个协程来处理这个请求。

在服务端接收到 http 请求时,会通过 Handler.ServeHTTP 方法进行处理. 而此处的 Handler 正是 gin.Engine,其处理请求的核心步骤如下:

  • 对于每笔 http 请求,会为其分配一个 gin.Context,在 handlers 链路中持续向下传递
  • 调用 Engine.handleHTTPRequest 方法,从路由树中获取 handlers 链,然后遍历调用
  • 处理完 http 请求后,会将 gin.Context 进行回收. 整个回收复用的流程基于对象池管理
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 从对象池中获取一个 contextc := engine.pool.Get().(*Context)// 重置/初始化 contextc.writermem.reset(w)c.Request = reqc.reset()// 处理 http 请求engine.handleHTTPRequest(c)// 把 context 放回对象池engine.pool.Put(c)
}

Engine.handleHTTPRequest 方法核心步骤分为三步:

  • 根据 http method 取得对应的 methodTree
  • 根据 path 从 methodTree 中找到对应的 handlers 链
  • 将 handlers 链注入到 gin.Context 中,通过 Context.Next 方法按照顺序遍历调用 handler

三.路由实现原理

3.1 压缩前缀树(radix tree)

再说压缩前缀树之前,先来说说前缀树:

前缀树也称Trie树或字典树,是一种基于字符串公共前缀构建树形结构,来降低查询时间和提高效率的目的。前缀树一般用于统计和排序大量的字符串,其核心思想是空间换时间。

前缀树有三个重要特性:

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
  • 从根节点到某一节点路径上所有字符连接起来,就是该节点对应的字符串。
  • 每个节点任意子节点包含的字符都不相同。

如下是普通前缀树的结构:

接下来,看看什么是压缩前缀树。

压缩前缀树又称基数树或 radix 树,是对前缀树的改良版本,优化点主要在于空间的节省

gin框架就采用的是压缩前缀树实现。

他和普通前缀树的区别就是倘若某个子节点是其父节点的唯一孩子,则与父节点进行合并。

3.2 为什么使用前缀树?

我们一般会将前缀树与哈希表结构进行对比,实际上标准库采用的就是哈希表实现。哈希表实现简单粗暴,但是有一些缺点,不太适合作为通用的路由结构。如:

  1. 哈希表实现只支持简单的路径,不支持路径参数和通配
  2. 路由的数量一般是有限的,使用map的优势并不明显
  3. 哈希表需要存储完整的路径,相比较而言前缀树存储公共前缀只需要一个节点,空间效率更高

3.3 补偿机制

在 Gin 路由树中还使用一种补偿策略,在组装路由树时,会将注册路由句柄数量更多的 child node 摆放在 children 数组更靠前的位置.

这是因为某个链路注册的 handlers 句柄数量越多,一次匹配操作所需要花费的时间就越长,且被匹配命中的概率就越大,因此应该被优先处理.

3.4 核心数据结构

下面聊一下路由树的数据结构,对应于 9 种 http method,共有 9 棵 methodTree. 每棵 methodTree 会通过 root 指向 radix tree 的根节点.

type methodTree struct {method stringroot   *node
}

具体的内容就不再介绍了,有兴趣可以下去了解一下

四.gin.Context

接下来来看一下gin框架的上下文,已经它是如何承接http的吧

4.1 核心数据结构

gin.Context 的定位是对应于一次 http 请求,贯穿于整条 handlersChain 调用链路的上下文,其中包含了如下核心字段:

  • Request/Writer:http 请求和响应的 reader、writer 入口
  • handlers:本次 http 请求对应的处理函数链
  • index:当前的处理进度,即处理链路处于函数链的索引位置
  • engine:Engine 的指针
  • mu:用于保护 map 的读写互斥锁
  • Keys:缓存 handlers 链上共享数据的 map
type Context struct {// ...// http 请求参数Request   *http.Request// http 响应 writerWriter    ResponseWriter// ...// 处理函数链handlers HandlersChain// 当前处于处理函数链的索引index    int8engine       *Engine// ...// 读写锁,保证并发安全mu sync.RWMutex// key value 对存储 mapKeys map[string]any// ..
}

4.2 复用策略

gin.Context 作为处理 http 请求的通用数据结构,不可避免地会被频繁创建和销毁. 为了缓解 GC 压力,gin 中采用对象池 sync.Pool 进行 Context 的缓存复用,处理流程如下:

  • • http 请求到达时,从 pool 中获取 Context,倘若池子已空,通过 pool.New 方法构造新的 Context 补上空缺
  • • http 请求处理完成后,将 Context 放回 pool 中,用以后续复用

sync.Pool 并不是真正意义上的缓存,将其称为回收站或许更加合适,放入其中的数据在逻辑意义上都是已经被删除的,但在物理意义上数据是仍然存在的,这些数据可以存活两轮 GC 的时间,在此期间倘若有被获取的需求,则可以被重新复用.

看一下gin.Context的回收和分配时机

gin.Context 分配与回收的时机是在 gin.Engine 处理 http 请求的前后,位于 Engine.ServeHTTP 方法当中:

  • 从池中获取 Context
  • 重置 Context 的内容,使其成为一个空白的上下文
  • 调用 Engine.handleHTTPRequest 方法处理 http 请求
  • 请求处理完成后,将 Context 放回池中
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 从对象池中获取一个 contextc := engine.pool.Get().(*Context)// 重置/初始化 contextc.writermem.reset(w)c.Request = reqc.reset()// 处理 http 请求engine.handleHTTPRequest(c)// 把 context 放回对象池engine.pool.Put(c)
}

4.3 使用时机

(1)handlesChain 入口

在 Engine.handleHTTPRequest 方法处理请求时,会通过 path 从 methodTree 中获取到对应的 handlers 链,然后将 handlers 注入到 Context.handlers 中,然后启动 Context.Next 方法开启 handlers 链的遍历调用流程.

func (engine *Engine) handleHTTPRequest(c *Context) {// ...t := engine.treesfor i, tl := 0, len(t); i < tl; i++ {if t[i].method != httpMethod {continue}root := t[i].root        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)// ...if value.handlers != nil {c.handlers = value.handlersc.fullPath = value.fullPathc.Next()c.writermem.WriteHeaderNow()return}// ...}// ...
}

(2)handlesChain 遍历调用

推进 handlers 链调用进度的方法正是 Context.Next. 可以看到其中以 Context.index 为索引,通过 for 循环依次调用 handlers 链中的 handler.

func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}
}

由于 Context 本身会暴露于调用链路中,因此用户可以在某个 handler 中通过手动调用 Context.Next 的方式来打断当前 handler 的执行流程,提前进入下一个 handler 的处理中.

由于此时本质上是一个方法压栈调用的行为,因此在后置位 handlers 链全部处理完成后,最终会回到压栈前的位置,执行当前 handler 剩余部分的代码逻辑.

结合下面的代码示例来说,用户可以在某个 handler 中,于调用 Context.Next 方法的前后分别声明前处理逻辑和后处理逻辑,这里的“前”和“后”相对的是后置位的所有 handler 而言.

func myHandleFunc(c *gin.Context){// 前处理preHandle()  c.Next()// 后处理postHandle()
}

此外,用户可以在某个 handler 中通过调用 Context.Abort 方法实现 handlers 链路的提前熔断.

其实现原理是将 Context.index 设置为一个过载值 63,导致 Next 流程直接终止. 这是因为 handlers 链的长度必须小于 63,否则在注册时就会直接 panic. 因此在 Context.Next 方法中,一旦 index 被设为 63,则必然大于整条 handlers 链的长度,for 循环便会提前终止.

const abortIndex int8 = 63func (c *Context) Abort() {c.index = abortIndex
}

此外,用户还可以通过 Context.IsAbort 方法检测当前 handlerChain 是出于正常调用,还是已经被熔断.

func (c *Context) IsAborted() bool {return c.index >= abortIndex
}

注册 handlers,倘若 handlers 链长度达到 63,则会 panic

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {finalSize := len(group.Handlers) + len(handlers)// 断言 handlers 链长度必须小于 63assert1(finalSize < int(abortIndex), "too many handlers")// ...
}

4.4 共享数据存取

gin.Context 作为 handlers 链的上下文,还提供对外暴露的 Get 和 Set 接口向用户提供了共享数据的存取服务,相关操作都在读写锁的保护之下,能够保证并发安全.

type Context struct {// ...// 读写锁,保证并发安全mu sync.RWMutex// key value 对存储 mapKeys map[string]any
}
func (c *Context) Get(key string) (value any, exists bool) {c.mu.RLock()defer c.mu.RUnlock()value, exists = c.Keys[key]return
}
func (c *Context) Set(key string, value any) {c.mu.Lock()defer c.mu.Unlock()if c.Keys == nil {c.Keys = make(map[string]any)}c.Keys[key] = value
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

4、Sending Messages

本节介绍如何发送消息。Using KafkaTemplate本节介绍如何使用KafkaTemplate发送消息。OverviewKafkaTemplate封装了一个生产者&#xff0c;并提供了向Kafka主题发送数据的便利方法。以下列表显示了KafkaTemplate的相关方法&#xff1a;CompletableFuture<SendResult<K, V…

CSS长度单位问题

在 CSS 中&#xff0c;100px 的逻辑长度在不同分辨率的手机屏幕上是否表现一致&#xff0c;取决于 设备的像素密度&#xff08;devicePixelRatio&#xff09; 和 视口&#xff08;viewport&#xff09;的缩放设置。以下是详细分析&#xff1a;1. 核心概念 CSS 像素&#xff08;…

基于Java+SpringBoot的图书管理系统

源码编号&#xff1a;S606源码名称&#xff1a;基于SpringBoot的图书管理系统用户类型&#xff1a;双角色&#xff0c;用户、管理员数据库表数量&#xff1a;12 张表主要技术&#xff1a;Java、Vue、ElementUl 、SpringBoot、Maven运行环境&#xff1a;Windows/Mac、JDK1.8及以…

XTOM工业级蓝光三维扫描仪用于笔记本电脑背板模具全尺寸检测

镁合金具有密度小、强度高、耐腐蚀性好等优点&#xff0c;成为笔记本电脑外壳主流材料。冲压模具作为批量生产笔记本电脑镁合金背板的核心工具&#xff0c;其精度直接决定了产品的尺寸一致性、结构可靠性与外观品质。微米级模具误差可能在冲压过程中被放大至毫米级&#xff08;…

运维打铁: MongoDB 数据库集群搭建与管理

文章目录思维导图一、集群基础概念1. 分片集群2. 副本集二、集群搭建1. 环境准备2. 配置副本集步骤 1&#xff1a;修改配置文件步骤 2&#xff1a;启动 MongoDB 服务步骤 3&#xff1a;初始化副本集3. 配置分片集群步骤 1&#xff1a;配置配置服务器副本集步骤 2&#xff1a;启…

HCIP-Datacom Core Technology V1.0_5 OSPF特殊区域及其他特性

在前面的章节中&#xff0c;OSPF可以划分区域&#xff0c;减轻单区域里面LSDB的规模&#xff0c;从而减轻路由器的负荷&#xff0c;虽然OSPF能够划分区域&#xff0c;但是依旧需要维护域间路由和外部路由&#xff0c;这样随着网络规模的不断扩大&#xff0c;路由器所维护的LSDB…

实时开发IDE部署指南

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨杭州奥零数据科技官网&#xff…

深入解析 RAGFlow:文件上传到知识库的完整流程

在 RAGFlow 这样的检索增强生成&#xff08;RAG&#xff09;系统中&#xff0c;知识库是其核心。用户上传的文档如何高效、可靠地转化为可检索的知识&#xff0c;是系统稳定运行的关键。今天&#xff0c;我们就来深入探讨 RAGFlow 中文件上传到知识库的完整流程&#xff0c;揭秘…

cad_recognition 笔记

Hubch/cad_recognition | DeepWiki https://github.com/Hubch/cad_recognition winget install python.python.3.10 python -m venv venv micromamba activate ./venv pip install paddleocr2.9.0 pip install poetry pip install moviepy1.0.3 下次要用conda建环境 或者…

基于odoo17的设计模式详解---构建模式

大家好&#xff0c;我是你的Odoo技术伙伴。在Odoo开发中&#xff0c;创建一个简单的记录可能只需要一行 self.env[res.partner].create({name: New Partner})。但如果我们要创建一个复杂的对象&#xff0c;比如一个包含了特定上下文、具有多个可选配置、并且需要执行一系列关联…

暑假算法日记第四天

目标​&#xff1a;刷完灵神专题训练算法题单 阶段目标&#x1f4cc;&#xff1a;【算法题单】滑动窗口与双指针 LeetCode题目:2953. 统计完全子字符串1016. 子串能表示从 1 到 N 数字的二进制串其他: 今日总结 往期打卡 2953. 统计完全子字符串 跳转: 2953. 统计完全子字符串…

Linux 常用命令大全(2025简明版)

&#x1f9ed; 一、文件和目录操作命令说明ls列出目录内容ls -l以列表形式显示&#xff08;含权限&#xff09;cd /path切换目录pwd显示当前路径mkdir dir创建目录mkdir -p dir/subdir递归创建目录rm file删除文件rm -r dir删除目录&#xff08;递归&#xff09;rm -rf dir强制…

React Ref 指南:原理、实现与实践

前言 React Ref&#xff08;引用&#xff09;是React中一个强大而重要的概念&#xff0c;它为我们提供了直接访问DOM元素或组件实例的能力。虽然React推崇声明式编程和数据驱动的理念&#xff0c;但在某些场景下&#xff0c;我们仍需要直接操作DOM或访问组件实例。本文将深入探…

4.权重衰减(weight decay)

4.1 手动实现权重衰减 import torch from torch import nn from torch.utils.data import TensorDataset,DataLoader import matplotlib.pyplot as plt def synthetic_data(w,b,num_inputs):Xtorch.normal(0,1,size(num_inputs,w.shape[0]))yXwbytorch.normal(0,0.1,sizey.shap…

OpenCV开发-初始概念

第一章 OpenCV核心架构解析1.1 计算机视觉的基石OpenCV&#xff08;Open Source Computer Vision Library&#xff09;作为跨平台计算机视觉库&#xff0c;自1999年由Intel发起&#xff0c;已成为图像处理领域的标准工具。其核心价值体现在&#xff1a;跨平台性&#xff1a;支持…

LeetCode 930.和相同的二元子数组

给你一个二元数组 nums &#xff0c;和一个整数 goal &#xff0c;请你统计并返回有多少个和为 goal 的 非空 子数组。 子数组 是数组的一段连续部分。 示例 1&#xff1a; 输入&#xff1a;nums [1,0,1,0,1], goal 2 输出&#xff1a;4 解释&#xff1a; 有 4 个满足题目要求…

【论文解读】Referring Camouflaged Object Detection

论文信息 论文题目&#xff1a;Referring Camouflaged Object Detection 论文链接&#xff1a;https://arxiv.org/pdf/2306.07532 代码链接&#xff1a;https://github.com/zhangxuying1004/RefCOD 录用期刊&#xff1a;TPAMI 2025 论文单位&#xff1a;南开大学 ps&#xff1a…

Spring中过滤器和拦截器的区别及具体实现

在 Spring 框架中&#xff0c;过滤器&#xff08;Filter&#xff09; 和 拦截器&#xff08;Interceptor&#xff09; 都是用于处理 HTTP 请求的中间件&#xff0c;但它们在作用范围、实现方式和生命周期上有显著区别。以下是详细对比和实现方式&#xff1a;核心区别特性过滤器…

CANFD 数据记录仪在新能源汽车售后维修中的应用

一、前言随着新能源汽车市场如火如荼和新能源汽车电子系统的日益复杂&#xff0c;传统维修手段在面对复杂和偶发故障时往往捉襟见肘&#xff0c;CANFD 数据记录仪则凭借其独特优势&#xff0c;为售后维修带来新的解决方案。二、 详细介绍在新能源汽车领域&#xff0c;CANFD 数据…

某当CRM XlsFileUpload存在任意文件上传(CNVD-2025-10982)

免责声明 本文档所述漏洞详情及复现方法仅限用于合法授权的安全研究和学术教育用途。任何个人或组织不得利用本文内容从事未经许可的渗透测试、网络攻击或其他违法行为。使用者应确保其行为符合相关法律法规,并取得目标系统的明确授权。 前言: 我们建立了一个更多,更全的…