【GO基础学习】gin框架路由详解

文章目录

  • gin框架路由详解
    • (1)go mod tidy
    • (2)r := gin.Default()
    • (3)r.GET()
      • 路由注册
    • (4)r.Run()
      • 路由匹配
    • 总结


gin框架路由详解

先创建一个项目,编写一个简单的demo,对这个demo进行讲解。

  1. 创建一个go的项目,采用GoLand:

在这里插入图片描述
2. 在该项目下创建一个main.go文件:

package mainimport ("net/http""github.com/gin-gonic/gin"
)func main() {r := gin.Default()r.GET("/", func(c *gin.Context) {c.String(http.StatusOK, "hello word")})r.Run(":8000")
}

上面就是一个非常简单的gin的使用,逐行代码解读,在解读前,需要先下载gin库,写入main.go后,在terminal执行go mod tidy命令就会添加main里面的gin库。

(1)go mod tidy

  • 添加缺失的依赖

如果你的代码中引用了某些依赖(通过 import),但它们没有被记录在 go.mod 文件中,go mod tidy 会自动将这些缺失的依赖添加到 go.mod 文件中。

  • 移除未使用的依赖

如果你的代码中不再使用某些依赖(即没有通过 import 引用),go mod tidy 会从 go.mod 文件中移除这些无用的依赖。

  • 更新 go.sum 文件

go mod tidy 会检查 go.sum 文件(存储模块的校验和)是否与 go.mod 文件一致:

​ -如果某些依赖的校验和缺失,它会添加。

​ -如果某些校验和多余(对应的依赖已被移除),它会删除。

(2)r := gin.Default()

创建默认的Gin引擎,点进这个方法查看源码:

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

返回了Engine的指针结构体,还包括一些日志和中断复原的操作。

关于Engine结构体:【是 Gin 框架的核心结构体,它既是路由表的管理器,也是 HTTP 服务的入口】

type Engine struct {RouterGrouptrees       methodTrees  // 每种 HTTP 方法对应的路由树maxParams   uint16       // 路由参数最大数量maxSections uint16       // 路由路径最大分段数量handlers404 HandlersChain // 404 处理函数链// 其他字段...
}
  • trees: 存储路由表的核心字段,每种 HTTP 方法有一棵对应的 Radix 树。

  • RouterGroup: 用于管理路由组和中间件。

  • handlers404: 默认的 404 错误处理。

关于trees是路由规则的核心,存储路由表,每种 HTTP 方法有一棵对应的 Radix 树。
(1)Radix 树
公共前缀的树结构,是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例:
在这里插入图片描述
(2)methodTrees

type methodTree struct {method stringroot   *node
}type methodTrees []methodTree

method是http的类型,每个路由路径的片段都由一个node节点构成:

type node struct {path      string        // 当前节点的路径部分indices   string        // 子节点的索引,用于快速查找children  []*node       // 子节点handlers  HandlersChain // 当前节点的处理函数priority  uint32        // 优先级,用于优化匹配顺序wildChild bool          // 是否包含通配符子节点nType     nodeType      // 节点类型: static, param, catchAll
}

path: 存储路径片段。

indices: 子节点索引,表示每个子节点的第一个字符,用于快速查找。

handlers: 当前节点绑定的处理函数。

wildChild: 是否有动态或通配符子节点。

nType:

  • static: 静态路径节点。
  • param: 动态路径节点(如 :id)。
  • catchAll: 通配符节点(如 *filepath)。

(3)r.GET()

Gin 的路由实现主要分为路由注册路由匹配两部分。

路由注册

点进GET方法:

// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {return group.handle(http.MethodGet, relativePath, handlers)
}

handle方法:

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {absolutePath := group.calculateAbsolutePath(relativePath)handlers = group.combineHandlers(handlers)group.engine.addRoute(httpMethod, absolutePath, handlers)return group.returnObj()
}

addRoute方法:【注册路由的核心函数】

func (e *Engine) addRoute(method, path string, handlers HandlersChain) {root := e.trees.get(method) // 获取当前方法对应的路由树if root == nil {root = new(node)       // 如果路由树不存在,创建新的树e.trees = append(e.trees, methodTree{method: method, root: root})}root.addRoute(path, handlers) // 将路径插入到 Radix 树中
}

Radix 树节点插入逻辑:addRoute in node
在 node 中的 addRoute 方法负责将路径拆分并插入到树中:

// addRoute 将具有给定句柄的节点添加到路径中。
// 不是并发安全的
func (n *node) addRoute(path string, handlers HandlersChain) {fullPath := pathn.priority++numParams := countParams(path)  // 数一下参数个数// 空树就直接插入当前节点if len(n.path) == 0 && len(n.children) == 0 {n.insertChild(numParams, path, fullPath, handlers)n.nType = rootreturn}parentFullPathIndex := 0walk:for {// 更新当前节点的最大参数个数if numParams > n.maxParams {n.maxParams = numParams}// 找到最长的通用前缀// 这也意味着公共前缀不包含“:”"或“*” /// 因为现有键不能包含这些字符。i := longestCommonPrefix(path, n.path)// 分裂边缘(此处分裂的是当前树节点)// 例如一开始path是search,新加入support,s是他们通用的最长前缀部分// 那么会将s拿出来作为parent节点,增加earch和upport作为child节点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, //子节点优先级-1fullPath:  n.fullPath,}// Update maxParams (max of all children)for _, v := range child.children {if v.maxParams > child.maxParams {child.maxParams = v.maxParams}}n.children = []*node{&child}// []byte for proper unicode char conversion, see #65n.indices = string([]byte{n.path[i]})n.path = path[:i]n.handlers = niln.wildChild = falsen.fullPath = fullPath[:parentFullPathIndex+i]}// 将新来的节点插入新的parent节点作为子节点if i < len(path) {path = path[i:]if n.wildChild {  // 如果是参数节点parentFullPathIndex += len(n.path)n = n.children[0]n.priority++// Update maxParams of the child nodeif numParams > n.maxParams {n.maxParams = numParams}numParams--// 检查通配符是否匹配if len(path) >= len(n.path) && n.path == path[:len(n.path)] {// 检查更长的通配符, 例如 :name and :namesif len(n.path) >= len(path) || path[len(n.path)] == '/' {continue walk}}pathSeg := pathif n.nType != catchAll {pathSeg = strings.SplitN(path, "/", 2)[0]}prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.pathpanic("'" + pathSeg +"' in new path '" + fullPath +"' conflicts with existing wildcard '" + n.path +"' in existing prefix '" + prefix +"'")}// 取path首字母,用来与indices做比较c := path[0]// 处理参数后加斜线情况if n.nType == param && c == '/' && len(n.children) == 1 {parentFullPathIndex += len(n.path)n = n.children[0]n.priority++continue walk}// 检查路path下一个字节的子节点是否存在// 比如s的子节点现在是earch和upport,indices为eu// 如果新加一个路由为super,那么就是和upport有匹配的部分u,将继续分列现在的upport节点for i, max := 0, len(n.indices); i < max; i++ {if c == n.indices[i] {parentFullPathIndex += len(n.path)i = n.incrementChildPrio(i)n = n.children[i]continue walk}}// 否则就插入if c != ':' && c != '*' {// []byte for proper unicode char conversion, see #65// 注意这里是直接拼接第一个字符到n.indicesn.indices += string([]byte{c})child := &node{maxParams: numParams,fullPath:  fullPath,}// 追加子节点n.children = append(n.children, child)n.incrementChildPrio(len(n.indices) - 1)n = child}n.insertChild(numParams, path, fullPath, handlers)return}// 已经注册过的节点if n.handlers != nil {panic("handlers are already registered for path '" + fullPath + "'")}n.handlers = handlersreturn}
}

整个路由树构造的详细过程:

(1)第一次注册路由,例如注册search
(2)继续注册一条没有公共前缀的路由,例如blog
(3)注册一条与先前注册的路由有公共前缀的路由,例如support

路由注册示例:

package mainimport ("github.com/gin-gonic/gin"
)func main() {r := gin.Default()// 注册静态路由r.GET("/hello", func(c *gin.Context) {c.String(200, "Hello, World!")})// 注册动态路由r.GET("/user/:id", func(c *gin.Context) {id := c.Param("id")c.String(200, "User ID: %s", id)})// 注册通配符路由r.GET("/static/*filepath", func(c *gin.Context) {filepath := c.Param("filepath")c.String(200, "Filepath: %s", filepath)})r.Run(":8080")
}

(4)r.Run()

路由匹配

路由匹配是根据请求路径在 Radix 树中查找对应节点并执行处理函数的过程。

核心代码:getValue

getValue 方法负责在 Radix 树中查找路径:
(1)Run()方法:

/ Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {defer func() { debugPrintError(err) }()if engine.isUnsafeTrustedProxies() {debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +"Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.")}address := resolveAddress(addr)debugPrint("Listening and serving HTTP on %s\n", address)err = http.ListenAndServe(address, engine.Handler())return
}

(2)Handler()方法处理类:

func (engine *Engine) Handler() http.Handler {if !engine.UseH2C {return engine}h2s := &http2.Server{}return h2c.NewHandler(engine, h2s)
}

(3)http.Handler

type Handler interface {ServeHTTP(ResponseWriter, *Request)
}

(4)ServeHTTP实现:

// gin.go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {// 这里使用了对象池c := engine.pool.Get().(*Context)// 这里有一个细节就是Get对象后做初始化c.writermem.reset(w)c.Request = reqc.reset()engine.handleHTTPRequest(c)  // 我们要找的处理HTTP请求的函数engine.pool.Put(c)  // 处理完请求后将对象放回池子
}

(5)handleHTTPRequest方法

// gin.go
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// 在路由树中根据path查找value := root.getValue(rPath, c.Params, unescape)if value.handlers != nil {c.handlers = value.handlersc.Params = value.paramsc.fullPath = value.fullPathc.Next()  // 执行函数链条c.writermem.WriteHeaderNow()return}c.handlers = engine.allNoRouteserveError(c, http.StatusNotFound, default404Body)
}

(6)getValue方法
路由匹配是由节点的 getValue方法实现的。getValue根据给定的路径(键)返回nodeValue值,保存注册的处理函数和匹配到的路径参数数据。
如果找不到任何处理函数,则会尝试TSR(尾随斜杠重定向)。

// tree.gotype nodeValue struct {handlers HandlersChainparams   Params  // []Paramtsr      boolfullPath string
}func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {value.params = po
walk: // Outer loop for walking the treefor {prefix := n.pathif path == prefix {// 我们应该已经到达包含处理函数的节点。// 检查该节点是否注册有处理函数if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if path == "/" && n.wildChild && n.nType != root {value.tsr = truereturn}// 没有找到处理函数 检查这个路径末尾+/ 是否存在注册函数indices := n.indicesfor i, max := 0, len(indices); i < max; i++ {if indices[i] == '/' {n = n.children[i]value.tsr = (len(n.path) == 1 && n.handlers != nil) ||(n.nType == catchAll && n.children[0].handlers != nil)return}}return}if len(path) > len(prefix) && path[:len(prefix)] == prefix {path = path[len(prefix):]// 如果该节点没有通配符(param或catchAll)子节点// 我们可以继续查找下一个子节点if !n.wildChild {c := path[0]indices := n.indicesfor i, max := 0, len(indices); i < max; i++ {if c == indices[i] {n = n.children[i] // 遍历树continue walk}}// 没找到// 如果存在一个相同的URL但没有末尾/的叶子节点// 我们可以建议重定向到那里value.tsr = path == "/" && n.handlers != nilreturn}// 根据节点类型处理通配符子节点n = n.children[0]switch n.nType {case param:// find param end (either '/' or path end)end := 0for end < len(path) && path[end] != '/' {end++}// 保存通配符的值if cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // 在预先分配的容量内扩展slicevalue.params[i].Key = n.path[1:]val := path[:end]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(val); err != nil {value.params[i].Value = val // fallback, in case of error}} else {value.params[i].Value = val}// 继续向下查询if end < len(path) {if len(n.children) > 0 {path = path[end:]n = n.children[0]continue walk}// ... but we can'tvalue.tsr = len(path) == end+1return}if value.handlers = n.handlers; value.handlers != nil {value.fullPath = n.fullPathreturn}if len(n.children) == 1 {// 没有找到处理函数. 检查此路径末尾加/的路由是否存在注册函数// 用于 TSR 推荐n = n.children[0]value.tsr = n.path == "/" && n.handlers != nil}returncase catchAll:// 保存通配符的值if cap(value.params) < int(n.maxParams) {value.params = make(Params, 0, n.maxParams)}i := len(value.params)value.params = value.params[:i+1] // 在预先分配的容量内扩展slicevalue.params[i].Key = n.path[2:]if unescape {var err errorif value.params[i].Value, err = url.QueryUnescape(path); err != nil {value.params[i].Value = path // fallback, in case of error}} else {value.params[i].Value = path}value.handlers = n.handlersvalue.fullPath = n.fullPathreturndefault:panic("invalid node type")}}// 找不到,如果存在一个在当前路径最后添加/的路由// 我们会建议重定向到那里value.tsr = (path == "/") ||(len(prefix) == len(path)+1 && prefix[len(path)] == '/' &&path == prefix[:len(prefix)-1] && n.handlers != nil)return}
}

Radix 树的路径匹配过程:

package mainimport ("fmt"
)type node struct {path     stringchildren []*nodehandlers func()
}func (n *node) addRoute(path string, handler func()) {child := &node{path: path, handlers: handler}n.children = append(n.children, child)
}func (n *node) getRoute(path string) func() {for _, child := range n.children {if child.path == path {return child.handlers}}return nil
}func main() {root := &node{}root.addRoute("/hello", func() {fmt.Println("Hello, World!")})handler := root.getRoute("/hello")if handler != nil {handler() // 输出:Hello, World!} else {fmt.Println("Route not found!")}
}

总结

  1. 创建路由表
    • 每种 HTTP 方法有独立的 Radix 树。
    • 路由通过 addRoute 插入到对应的树中。
  2. 处理 HTTP 请求
    • Gin 的入口是 EngineServeHTTP 方法。
    • 根据请求方法和路径查找路由节点:
      • 如果找到,执行绑定的处理函数。
      • 如果未找到,执行 404 处理函数。
  3. 分发请求
    • 匹配成功的路由节点的处理函数会被依次执行,支持中间件链。

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

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

相关文章

vue之axios基本使用

文章目录 1. axios 网络请求库2. axiosvue 1. axios 网络请求库 <body> <input type"button" value"get请求" class"get"> <input type"button" value"post请求" class"post"> <!-- 官网提供…

ubuntu快速入门

1.进入某个文件夹 cd workspace/2.tab自动补全 3.列出当前文件夹所有文件 ls列出所有文件包括隐藏文件 ls -a 4.创建文件夹 mkdir linuxLearn 5.创建文件 gedit command.sh在commmand.sh键入 echo hello echo hi? echo how are you? PS:touch hello.txt(也可以创建新…

优化 invite_codes 表的 SQL 创建语句

-- auto-generated definition create table invite_codes (id int auto_incrementprimary key,invite_code varchar(6) not null comment 邀请码&#xff0c;6位整数&#xff0c;确保在有效期内…

FATE-LLM简介;FATE-LLM集成了多种参数高效微调方法

FATE-LLM简介 FATE-LLM是一个支持联邦大语言模型训练的框架,其架构及核心技术原理如下: 架构概述 FATE-LLM主要由模型层、参数高效微调层、隐私保护与安全机制、通信与聚合模块等组成,致力于在保护数据隐私的前提下,利用联邦学习技术整合各方数据与算力资源,提升大语言模…

小程序租赁系统构建指南与市场机会分析

内容概要 在当今竞争激烈的市场环境中&#xff0c;小程序租赁系统正崭露头角&#xff0c;成为企业转型与创新的重要工具。通过这个系统&#xff0c;商户能够快速推出自己的小程序&#xff0c;无需从头开发&#xff0c;节省了大量时间和资金。让我们来看看这个系统的核心功能吧…

数据库系列之分布式数据库下误删表怎么恢复?

数据的完整性是数据库可用性的基本功能&#xff0c;在实际应用数据库变更操作过程中可能因为误操作导致误删表或者truncate操作影响业务的正常访问。本文介绍了分布式数据库中在误删表场景下的数据恢复方案&#xff0c;并进行了对比。 1、数据库误删表恢复方案 应用数据的完整…

论文阅读:Towards Faster Deep Graph Clustering via Efficient Graph Auto-Encoder

论文地址&#xff1a;Towards Faster Deep Graph Clustering via Efficient Graph Auto-Encoder | ACM Transactions on Knowledge Discovery from Data 代码地址&#xff1a; https://github.com/Marigoldwu/FastDGC 摘要 深度图聚类&#xff08;Deep Graph Clustering, DGC…

Python爬虫教程——7个爬虫小案例(附源码)_爬虫实例

本文介绍了7个Python爬虫小案例&#xff0c;包括爬取豆瓣电影Top250、猫眼电影Top100、全国高校名单、中国天气网、当当网图书、糗事百科段子和新浪微博信息&#xff0c;帮助读者理解并实践Python爬虫基础知识。 包含编程资料、学习路线图、源代码、软件安装包等&#xff01;【…

BMS存储模块的设计

目的 电池管理系统中存在着数据本地存储的要求&#xff0c;保证控制器重新上电后能够根据存储器中的一些参数恢复控制状态&#xff0c;和信息的下电存储1.继电器故障信息的存储。2. 系统性故障的存储。3.SOC、SOH相关信息的存储。4.均衡参数的存储。5.系统时间信息。6.出厂信息…

Python爬取城市天气信息,并存储到csv文件中

1.爬取的网址为&#xff1a;天气网 (weather.com.cn) 2.需要建立Weather.txt文件&#xff0c;并在里面加入如下形式的字段&#xff1a; 101120701济宁 101010100北京 3.代码运行后&#xff0c;在命令行输入Weather.txt文件中添加过的城市&#xff0c;如&#xff1a;济宁。 …

MySQL线上事故:使用`WHERE`条件`!=xxx`无法查询到NULL数据

前言 在一次 MySQL 的线上查询操作中&#xff0c;因为 ! 的特性导致未能正确查询到为 NULL 的数据&#xff0c;险些引发严重后果。本文将详细解析 NULL 在 SQL 中的行为&#xff0c;如何避免类似问题&#xff0c;并提供实际操作建议。 1. 为什么NULL会查询不到&#xff1f; 在…

Solidworks打开无法获得许可,提示(-15,10,10061)错误解决办法

参考文章&#xff1a; https://blog.csdn.net/2301_81263647/article/details/140904773

【电路理论四】正弦电流电路

正弦电流 正弦量是随时间按正弦规律变动的电路变量。 随时间按正弦规律变动的电流称为正弦电流。 正弦电流的瞬时值表达式&#xff1a; 称为正弦电流的三要素。 分别为振幅/幅值&#xff0c;角频率&#xff0c;初相。 幅值为正弦电流的最大值&#xff0c;恒为正。 为正弦电…

解决Windows无法同时使用有线网和无线网WIFI的问题

参考资料 电脑无线网wifi和有线网同时使用&#xff08;内网外网同时使用&#xff09;用route命令解决Wifi和网卡不能同时上内外网问题 解决方法 对于Windows系统同时连接有线网和无线网Wifi时&#xff0c;只会有一个网关生效&#xff0c;因此我们需要手动通过route命令设置网…

FOC控制原理-ADC采样时机

0、文章推荐 SimpleFOC移植STM32&#xff08;五&#xff09;—— 电流采样及其变换_极对数对电流采样的影响-CSDN博客 FOC 电流采样方案对比&#xff08;单电阻/双电阻/三电阻&#xff09; - 知乎 (zhihu.com) FOC中的三种电流采样方式&#xff0c;你真的会选择吗&#xff1f;…

uniapp:编译微信、h5都正常的,编译钉钉小程序无法找到页面

我们在做uniapp的开发的时候遇到很多莫名其妙的问题&#xff0c;其中就有个很奇葩的问题&#xff0c;我们开发的过程中使用微信小程序/h5调试都是正常编译正常跳转&#xff0c;然后在要上架的时候&#xff0c;编译钉钉小程序成功&#xff0c;但是访问页面无法访问&#xff0c; …

zentao ubuntu上安装

#下载ZenTaoPMS-21.2-zbox_amd64.tar.gz&#xff08;https://www.zentao.net/downloads.html&#xff09; https://dl.zentao.net/zentao/21.2/ZenTaoPMS-21.2-zbox_amd64.tar.gzcd /opt tar -zxvf ZenTaoPMS-21.2-zbox_amd64.tar.gz#启动 /opt/zbox/zbox start /opt/zbox/zbox…

REDIS2.0

string list hash set 无序集合 声明一个key&#xff0c;键里面的值是元素&#xff0c;元素的类型是string 元素的值是唯一的&#xff0c;不能重复 多个集合类型之间可以进行并集&#xff0c;交集&#xff0c;集查的运算 sadd test1 a b c c d &#xff1a;添加5个元素&am…

【论文阅读】DebSDF:深入研究神经室内场景重建的细节和偏差

【论文阅读】DebSDF&#xff1a;深入研究神经室内场景重建的细节和偏差 Abstract3.METHOD3.1 Preliminaries3.2 Uncertainty Guided Prior Filtering3.3 Uncertainty-Guided Ray Sampling3.4 Uncertainty-Guided Smooth Regularization3.5 Bias-aware SDF to Density Transform…

微服务SpringCloud分布式事务之Seata

视频教程&#xff1a;https://www.bilibili.com/video/BV16P63Y3ESq 效果演示 准备的微服务项目调用的链路如下&#xff1a; 文字描述&#xff1a; gateway模块接收到请求&#xff0c;并发送到order订单模块order订单模块接收到请求&#xff0c;新增一个订单数据后发送一个…