GIN框架路由的实现原理

news2024/11/11 22:16:49

文章目录

  • 首先回顾一下gin框架的路由如何使用的
  • 从源码分析一下gin框架
  • gin的路由实现
    • 前缀树
    • 前缀树的实现
    • 压缩前缀树--Radix Trie
    • Trie VS Map

首先回顾一下gin框架的路由如何使用的

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	//创建一个gin Engine ,Engine实现了net/http包下Handler接口,本质上是一个http Handler
	r := gin.Default()

	//注册中间件
	r.Use(middleware1)
	r.Use(middleware2)

	//注册一个path为/hello的处理函数,将/hello域名下的get请求路由到后面的handler函数来处理
	r.GET("/hello", handler)
	r.Run()

}
func middleware1(c *gin.Context) {
	fmt.Println("Executing middleware1")
	c.Next() //将控制权传递给下一个中间件或者处理函数
	//c.Abort() //在请求处理过程中发现错误或异常情况时,立即终止处理并返回错误信息,后续的处理程序或者中间件不在执行
	//Executing middleware1
	//middle1 end

	fmt.Println("middle1 end")
}

func middleware2(c *gin.Context) {
	fmt.Println("Executing middleware2")
	c.Next()
	fmt.Println("middle2 end")
}

func handler(c *gin.Context) {
	fmt.Println("Executing handler")
	fmt.Println("handler end")
	c.JSON(http.StatusOK, gin.H{"message": "hello word"})

}

/*
Executing middleware1
Executing middleware2
Executing handler
handler end
middle2 end
middle1 end

*/

运行程序
在浏览器输入“http://127.0.0.1:8080/hello”可以看到
在这里插入图片描述
http的请求有9种,GET\HEAD\POST\PUT\PATCH\DELETE\CONNECT\OPTIONS\TRACE

从源码分析一下gin框架

首先通过“gin.Default()”创建一个Engine实例并附带有“Logger\Recovery”中间件

// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

Default()实际上是调用了New(),并使用use注册“Logger\Recovery”两个中间件

func New() *Engine {
	debugPrintWARNINGNew()
	engine := &Engine{
		RouterGroup: RouterGroup{
			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),
		delims:                 render.Delims{Left: "{{", Right: "}}"},
		secureJSONPrefix:       "while(1);",
		trustedProxies:         []string{"0.0.0.0/0", "::/0"},
		trustedCIDRs:           defaultTrustedCIDRs,
	}
	engine.RouterGroup.engine = engine
	engine.pool.New = func() any {
		return engine.allocateContext(engine.maxParams)
	}
	return engine
}

New()函数初始化Engine实例并返回,接下来看一下Engine struct都有些什么

type Engine struct {
	RouterGroup

	// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
	// handler for the path with (without) the trailing slash exists.
	// For example if /foo/ is requested but a route only exists for /foo, the
	// client is redirected to /foo with http status code 301 for GET requests
	// and 307 for all other request methods.
	RedirectTrailingSlash bool

	// RedirectFixedPath if enabled, the router tries to fix the current request path, if no
	// handle is registered for it.
	// First superfluous path elements like ../ or // are removed.
	// Afterwards the router does a case-insensitive lookup of the cleaned path.
	// If a handle can be found for this route, the router makes a redirection
	// to the corrected path with status code 301 for GET requests and 307 for
	// all other request methods.
	// For example /FOO and /..//Foo could be redirected to /foo.
	// RedirectTrailingSlash is independent of this option.
	RedirectFixedPath bool

	// HandleMethodNotAllowed if enabled, the router checks if another method is allowed for the
	// current route, if the current request can not be routed.
	// If this is the case, the request is answered with 'Method Not Allowed'
	// and HTTP status code 405.
	// If no other Method is allowed, the request is delegated to the NotFound
	// handler.
	HandleMethodNotAllowed bool

	// ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that
	// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
	// fetched, it falls back to the IP obtained from
	// `(*gin.Context).Request.RemoteAddr`.
	ForwardedByClientIP bool

	// AppEngine was deprecated.
	// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD
	// #726 #755 If enabled, it will trust some headers starting with
	// 'X-AppEngine...' for better integration with that PaaS.
	AppEngine bool

	// UseRawPath if enabled, the url.RawPath will be used to find parameters.
	UseRawPath bool

	// UnescapePathValues if true, the path value will be unescaped.
	// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
	// as url.Path gonna be used, which is already unescaped.
	UnescapePathValues bool

	// RemoveExtraSlash a parameter can be parsed from the URL even with extra slashes.
	// See the PR #1817 and issue #1644
	RemoveExtraSlash bool

	// RemoteIPHeaders list of headers used to obtain the client IP when
	// `(*gin.Engine).ForwardedByClientIP` is `true` and
	// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
	// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
	RemoteIPHeaders []string

	// TrustedPlatform if set to a constant of value gin.Platform*, trusts the headers set by
	// that platform, for example to determine the client IP
	TrustedPlatform string

	// MaxMultipartMemory value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
	// method call.
	MaxMultipartMemory int64

	// UseH2C enable h2c support.
	UseH2C bool

	// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
	ContextWithFallback bool

	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
	trustedCIDRs     []*net.IPNet
}

从结构体中发现,Engine有一个“sync.Pool”类型的pool变量:
sync.Pool是sync包下的一个内存池组件用来实现对象的复用,避免重复创建相同的对象,造成频繁的内存分配和gc,以达到提升程序性能的目的,虽然池子中的对象可以被复用,
但是sync.Pool并不会永久保存这个对象,池子中的对象会在一定时间后被gc回收,这个时间是随机的。所以用sync.Pool持久化存储对象不可取
sync.Pool本身是线程安全的,支持多个goroutine并发的往sync.Pool存取数据

sync.Pool的使用例子

package main
import (
	"fmt"
	"sync"
)

type Student struct {
	Name string
	Age  int
}

func main() {
	pool := sync.Pool{
		New: func() interface{} {
			return &Student{
				Name: "zhangsan",
				Age:  23,
			}
		},
	}
	st := pool.Get().(*Student)
	fmt.Println(st.Age, st.Name)
	fmt.Printf("%p\n", &st)
	
	pool.Put(st)

	st = pool.Get().(*Student)
	fmt.Println(st.Age, st.Name)
	fmt.Printf("%p\n", &st)

}
//23 zhangsan
//0xc00000a028
//23 zhangsan 
//0xc00000a028

Engine结构RouterGroup结构体,接下来看一下“RouterGroup”结构体

type RouterGroup struct {
	//HandlersChain defines a HandlerFunc slice
	//HandlerFunc defines the handler used by gin middleware as return value
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}

Engine中还有一个“methodTrees”类型的变量trees

trees            methodTrees
type methodTrees []methodTree
type methodTree struct{
	method string
	root *node
}
type node struct {
	path      string
	indices   string
	wildChild bool
	nType     nodeType
	priority  uint32
	children  []*node // child nodes, at most 1 :param style node at the end of the array
	handlers  HandlersChain
	fullPath  string
}

node 中最重要的两个结构就是

type node struct{
	wildChild bool
	children []*node
}
//其实这个就是前缀树实现的比较重要的两个数据变量

gin的路由实现

gin的每种请求,都是一个域名对应着一个路由处理函数,就是一种映射关系;直观上可以使用map存储这种映射关系,key存储域名,value存储域名对应的处理函数;
但实际上,gin路由底层实现这个映射使用的是压缩前缀树,首先介绍一下前缀树

前缀树

前缀树就是trie树,是一种树形结构,用于高效地存储和检索字符串集合。基本思想就是将字符串中的每个字符作为树的一个节点,从根节点开始,每个节点代表代表字符串中的一个前缀。在Trie树,每个节点都包含一个指向子节点的指针数组,数组的大小等于字符集的大小。如果某个节点代表的字符串是一个单词的结尾,可以在该节点上做一个标记。
以下是前缀树的一些特性:

  • 前缀匹配。前缀树可以高效地实现前缀匹配。给定一个前缀,可以在O(k)的时间复杂度内找到所有以该前缀开头的字符串,k是前缀的长度
  • 高效存储和检索。前缀树可以高效地存储和检索字符串集合。在插入和查找字符串时,时间复杂度为O(k),k是字符串的长度。相比于哈希表和二叉搜索树,前缀树在字符串存储和检索方面具有更好的性能
  • 消耗较大的空间。空间复杂度较高,每个节点都需要存储指向子节点的指针数组,节点的数量可能会非常多。空间复杂度为O(n*m)其中n为字符串集合中字符串的平均长度,m为字符串数量
  • 支持删除操作。删除操作相对比较复杂,删除一个字符串需要同时删除相应的节点,需要处理节点合并的情况。
  • 应用场景。单词查询,自动补全,模糊匹配

前缀树的实现

前缀树的实现可以参考力扣上的一道代码题
题目链接
大致的思路就是

go 语言实现

type Trie struct {
    trie [26]*Trie
    flag bool
}


func Constructor() Trie {
    return Trie{}
}


func (this *Trie) Insert(word string)  {
    tr:= this
    for i:=0;i<len(word);i++{
        if tr.trie[word[i]-'a']==nil{
            tr.trie[word[i]-'a'] = &Trie{}
        }
        tr=tr.trie[word[i]-'a']
    }
    tr.flag=true
}


func (this *Trie) Search(word string) bool {
    tr:=this.startpre(word)
    return tr!=nil && tr.flag
}


func (this *Trie) StartsWith(prefix string) bool {
    return this.startpre(prefix)!=nil
}
func (this *Trie)startpre(pre string)*Trie{
    tr:=this    
    for i:=0;i<len(pre);i++{
        if tr.trie[pre[i]-'a']==nil{
            return nil
        }
        tr=tr.trie[pre[i]-'a']
    }
    return tr
}


/**
 * Your Trie object will be instantiated and called as such:
 * obj := Constructor();
 * obj.Insert(word);
 * param_2 := obj.Search(word);
 * param_3 := obj.StartsWith(prefix);
 */

C++实现版本,由于C++没有回收内存的机制所以需要手动释放内存

class Trie {
public:
    ~Trie(){
        //析构函数释放内存
        del(this);
    }
    Trie():trie(26),flag(false){

    }
    
    void insert(string word) {
        Trie *tr = this;
        for(const char&c:word){
            if(!tr->trie[c-'a']) tr->trie[c-'a'] = new Trie();
            tr=tr->trie[c-'a'];
        }
        tr->flag=true;
    }
    
    bool search(string word) {
        Trie *tr = this;
        for(const char&c:word){
            if(!tr->trie[c-'a']) return false;
            tr=tr->trie[c-'a'];
        }
        return tr->flag;
    }
    
    bool startsWith(string prefix) {
        Trie *tr = this;
        for(const char&c:prefix){
            if(!tr->trie[c-'a']) return false;
            tr=tr->trie[c-'a'];
        }
        return true;
    }
private:
    vector<Trie*> trie;
    bool flag;
private:
    void del(Trie *tr){
        for(int i=0;i<26;++i){
            delete(tr->trie[i]);
            tr->trie[i]=nullptr;
        }
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */

解释一下其中的关键代码,del

	void del(Trie *tr){
		//由于每个Trie节点都有一个长度为26的指向子节点的指针数组,所以写了一个长度为26的for循环
        for(int i=0;i<26;++i){
        	//由于tr->trie[i]是一个Trie的数据类型变量,所以在delete tr->trie[i]的时候会触发tr-trie[i]的析构函数,往下递归的进行内存释放
            delete(tr->trie[i]);
            //释放完之后为了防止野指针,将tr->trie[i]设置为nullptr
            tr->trie[i]=nullptr;
        }
    }

压缩前缀树–Radix Trie

从上述的分析和代码实现可以看出,前缀树占用的空间特别大。那么为了优化空间的利用率,gin的路由采用了“压缩前缀树”。

使用如下方法进行压缩:

  • 合并相邻的只有一个子节点的节点:遍历前缀树,当发现一个节点只有一个子节点时,将该节点与其子节点合并。合并时,将子节点的字符添加到父节点的字符中,形成新的字符
  • 递归地压缩子树,在合并节点后,可能会出现新的节点也只有一个子节点的情况,可以递归的对子树进行压缩,直到无法压缩为止
  • 使用特殊字符表示合并,为了在压缩前缀树中表示节点的合并,可以使用一些特殊的字符进行标记。例如可以使用$表示节点的合并

接下来,利用数据结构可视化工具查看两个结构的不同
依次插入[“apple”,“apricot”,“banana”,“bat”]

前缀树的结构如下

在这里插入图片描述

压缩前缀树的结构如下

在这里插入图片描述

从两图可以看出,Radix Trie对Trie进行了压缩:遍历Trie发现某个节点只有一个子节点,就与子节点进行合并,减少指针数组的占用,优化空间

Trie VS Map

接下来说一下为什么不用map实现路径和路由函数之间的映射。

  • map并不支持模糊匹配和参数提取;前缀树可以根据前缀进行参数提取,而使用map进行参数提取需要额外的步骤
  • 前缀树可以根据前缀进行分组管理,而map不行

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

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

相关文章

使用策略模式重构审批

之前在公司与同时合作开发了一个考核系统&#xff0c;最后干完后整个代码也是合在了我这里&#xff0c;于是进行了codereview&#xff0c;进行优化代码&#xff0c;在审核这边&#xff0c;我先是拆分了公共代码&#xff0c;然后对重复的代码块进行了封装&#xff0c;但是审核这…

Flutter配置Android SDK路径

在使用VSCode作为开发Flutter的工具时&#xff0c;当选择调试设备时&#xff0c;通常看不到android的模拟器&#xff0c;只能看到Chrome之类的。 原因就是Flutter找不到Android的SDK路径&#xff0c;所以无法识别模拟器&#xff0c;我们用flutter doctor命令检查环境时&#xf…

论文《Link Prediction on Latent Heterogeneous Graphs》阅读

论文《Link Prediction on Latent Heterogeneous Graphs》阅读 论文概况IntroductionLHGNNA.语义嵌入a.节点级语义嵌入b.路径级语义嵌入 B.潜在异构上下文聚合a.上下文个性化b.上下文聚合 C.链路预测a.链路编码器b.损失函数 总结 论文概况 本文是2023年WWW上的一篇论文&#xf…

LeetCode 59. 螺旋矩阵 II【数组,模拟】中等

本文属于「征服LeetCode」系列文章之一&#xff0c;这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁&#xff0c;本系列将至少持续到刷完所有无锁题之日为止&#xff1b;由于LeetCode还在不断地创建新题&#xff0c;本系列的终止日期可能是永远。在这一系列刷题文章…

563. 二叉树的坡度

563. 二叉树的坡度 C代码&#xff1a; int sum;int dfs(struct TreeNode* root) {if (root NULL) {return 0;}int left dfs(root->left);int right dfs(root->right);sum fabs(left - right);return root->val left right; }int findTilt(struct TreeNode* roo…

【HTML5】语义化标签记录

前言 防止一个页面中全部都是div&#xff0c;或者ul li&#xff0c;在html5推出了很多语义化标签 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 常用语义化案例 一般我用的多的是header&#xff0c;main&#xff0c;footer 这些标签不难理解&#x…

G1 GC详解及设置

一、概述 G1 GC&#xff0c;全称Garbage-First Garbage Collector&#xff0c;在JDK1.7中引入了G1 GC&#xff0c;从JAVA 9开始&#xff0c;G1 GC是默认的GC算法。通过-XX:UseG1GC参数来启用。G1收集器是工作在堆内不同分区上的收集器&#xff0c;分区既可以是年轻代也可以是老…

ChatGLM2-6B微调实践-Lora方案

ChatGLM2-6B微调实践-Lora方案 环境部署Lora微调项目部署准备数据集修改训练脚本adapter推理模型合并与量化合并后的模型推理 微调过程中遇到的问题参考&#xff1a; 环境部署 安装Anaconda、CUDA、PyTorch 参考&#xff1a;ChatGLM2-6B微调实践-P-Tuning方案 Lora微调 项目…

基于opencv,卡尺工具

机器视觉尺寸测量项目中&#xff0c;测量工件尺寸中最基本的卡尺工具。 卡尺工具涉及到的最主要任务&#xff1a; 扫描边缘点&#xff0c;亚像素精度 拟合直线 实现了一个小demo&#xff0c;用来获取工件边缘&#xff0c;亚像素精度。 代码链接放下下面 https://download.cs…

饥荒服务器阿里云租用价格表一年和一个月收费报价表

饥荒阿里云服务器多少钱一个月&#xff1f;阿里云服务器价格9元一个月&#xff0c;阿里云轻量应用服务器2核2G3M带宽轻量服务器一年108元&#xff0c;2核4G4M带宽轻量服务器一年297.98元12个月&#xff1b;阿里云ECS云服务器e系列2核2G配置182元一年、2核4G配置365元一年、2核8…

苹果遭遇安全危机,应用商店曝出不良APP,或影响iPhone的销售

据澎湃新闻报道指苹果的App Store被曝出不良APP位居下载榜前列&#xff0c;这对于向来强调APP严格审核的苹果来说是巨大的打击&#xff0c;更影响向来被认为信息安全遥遥领先的名声&#xff0c;对当下正热销的iPhone15或造成打击。 据了解被曝的软件以“学习XX字母”为命名&…

Apache Shiro 漏洞复现

文章目录 Apache Shiro 漏洞复现1. Apache Shiro 1.2.4 反序列化漏洞1.1 漏洞描述1.2 漏洞原理1.3 漏洞复现1.3.1 环境启动 1.4 漏洞利用1.5 修复方案 Apache Shiro 漏洞复现 链接地址&#xff1a;Vulhub - Docker-Compose file for vulnerability environment 1. Apache Shi…

Chrome自动播放限制策略

原文链接&#xff1a;Chrome 自动播放限制策略 Web浏览器正在朝着更严格的自动播放策略发展&#xff0c;以便改善用户体验&#xff0c;最大限度地降低安装广告拦截器的积极性并减少昂贵和/或受限网络上的数据消耗。这些更改旨在为用户提供更大的播放控制权&#xff0c;并使开发…

ThreeJs中场景(scene)、 相机(camera)、渲染器(renderer)等方法类使用

ThreeJs笔记 简介 WebGL&#xff08;Web Graphics Library&#xff0c;Web图形库&#xff09;&#xff0c;是一个JavaScript API&#xff0c;可在任何兼容的Web浏览器中渲染高性能的交互式3D和2D图形&#xff0c;而无需使用插件 。 WebGL通过引入一个与OpenGL ES 2.0非常一致…

学编程,为什么优先推荐学Python?

编程&#xff0c;也就是用计算机语言来控制计算机的过程&#xff0c;是当今社会中一项非常重要和有用的技能。无论你是想从事科学研究、工程设计、商业管理、教育传播、艺术创作&#xff0c;还是其他任何领域&#xff0c;学习编程都可以给你带来很多好处。 本文将从以下几个方…

Maven 构建生命周期

目录 构建阶段由插件目标构成 Clean 生命周期 Default (Build) 生命周期 命令行调用 Site 生命周期 Maven 构建生命周期定义了一个项目构建跟发布的过程。 一个典型的 Maven 构建&#xff08;build&#xff09;生命周期是由以下几个阶段的序列组成的&#xff1a; 阶段 处…

express-generator快速构建node后端项目

express-generator是express官方团队开发者准备的一个快速生成工具&#xff0c;可以非常快速的生成一个基于express开发的框架基础应用。 npm安装 npm install express-generator -g初始化应用 express my_node_test 创建了一个名为 my_node_test 的express骨架项目通过 Exp…

Session 机制

一、Session 会话机制原理 Session&#xff08;会话&#xff09;机制是一种在 Web 应用程序中用来跟踪用户状态的技术。它通过在服务器端存储和管理用户信息&#xff0c;为每个用户分配一个唯一的会话标识符&#xff08;Session ID/Token&#xff09;&#xff0c;并将该标识符…

CMS难题待解?头部企业已领跑前装量产与集成趋势

对汽车智能化来说&#xff0c;又一项智能交互的科技新配置已经兴起。 今年9月初&#xff0c;阿维塔12在德国慕尼黑车展上全球首发亮相&#xff0c;作为一款纯电智能豪华轿跑&#xff0c;新车采用电子外后视镜&#xff08;CMS&#xff09;取代了传统外后视镜&#xff0c;为这款…

提供电商API接口,点击获取API文档及测试

提供电商API接口&#xff0c;点击获取API文档及测试&#xff0c;一键对接各大主流电商平台 随着电子商务的飞速发展&#xff0c;电商API接口在业务运营中发挥着越来越重要的作用。它们提供了高效、便捷的连接方式&#xff0c;使得不同系统之间能够无缝协同工作&#xff0c;提升…