go-zero 是如何做路由管理的?
原文链接: go-zero 是如何做路由管理的?
go-zero 是一个微服务框架,包含了 web 和 rpc 两大部分。
而对于 web 框架来说,路由管理是必不可少的一部分,那么本文就来探讨一下 go-zero 的路由管理是怎么做的,具体采用了哪种技术方案。
路由管理方案
路由管理方案有很多种,具体应该如何选择,应该根据使用场景,以及实现的难易程度做综合分析,下面介绍常见的三种方案。
注意这里只是做一个简单的概括性对比,更加详细的内容可以看这篇文章:HTTP Router 算法演进。
标准库方案
最简单的方案就是直接使用 map[string]func()
作为路由的数据结构,键为具体的路由,值为具体的处理方法。
// 路由管理数据结构type ServeMux struct {mu sync.RWMutex // 对象操作读写锁m map[string]muxEntry // 存储路由映射关系
}
这种方案优点就是实现简单,性能较高;缺点也很明显,占用内存更高,更重要的是不够灵活。
Trie Tree
Trie Tree 也称为字典树或前缀树,是一种用于高效存储和检索、用于从某个集合中查到某个特定 key 的数据结构。
Trie Tree 时间复杂度低,和一般的树形数据结构相比,Trie Tree 拥有更快的前缀搜索和查询性能。
和查询时间复杂度为 O(1)
常数的哈希算法相比,Trie Tree 支持前缀搜索,并且可以节省哈希函数的计算开销和避免哈希值碰撞的情况。
最后,Trie Tree 还支持对关键字进行字典排序。
Radix Tree
Radix Tree(基数树)是一种特殊的数据结构,用于高效地存储和搜索字符串键值对,它是一种基于前缀的树状结构,通过将相同前缀的键值对合并在一起来减少存储空间的使用。
Radix Tree 通过合并公共前缀来降低存储空间的开销,避免了 Trie Tree 字符串过长和字符集过大时导致的存储空间过多问题,同时公共前缀优化了路径层数,提升了插入、查询、删除等操作效率。
比如 Gin 框架使用的开源组件 HttpRouter 就是采用这个方案。
go-zero 路由规则
在使用 go-zero 开发项目时,定义路由需要遵守如下规则:
- 路由必须以
/
开头 - 路由节点必须以
/
分隔 - 路由节点中可以包含
:
,但是:
必须是路由节点的第一个字符,:
后面的节点值必须要在结请求体中有path tag
声明,用于接收路由参数 - 路由节点可以包含字母、数字、下划线、中划线
接下来就让我们深入到源码层面,相信看过源码之后,你就会更懂这些规则的意义了。
go-zero 源码实现
首先需要说明的是,底层数据结构使用的是二叉搜索树,还不是很了解的同学可以看这篇文章:使用 Go 语言实现二叉搜索树
节点定义
先看一下节点定义:
// core/search/tree.goconst (colon = ':'slash = '/'
)type (// 节点node struct {item interface{}children [2]map[string]*node}// A Tree is a search tree.Tree struct {root *node}
)
重点说一下 children
,它是一个包含两个元素的数组,元素 0
存正常路由键,元素 1
存以 :
开头的路由键,这些是 url 中的变量,到时候需要替换成实际值。
举一个例子,有这样一个路由 /api/:user
,那么 api
会存在 children[0]
,user
会存在 children[1]
。
具体可以看看这段代码:
func (nd *node) getChildren(route string) map[string]*node {// 判断路由是不是以 : 开头if len(route) > 0 && route[0] == colon {return nd.children[1]}return nd.children[0]
}
路由添加
// Add adds item to associate with route.
func (t *Tree) Add(route string, item interface{}) error {// 需要路由以 / 开头if len(route) == 0 || route[0] != slash {return errNotFromRoot}if item == nil {return errEmptyItem}// 把去掉 / 的路由作为参数传入err := add(t.root, route[1:], item)switch err {case errDupItem:return duplicatedItem(route)case errDupSlash:return duplicatedSlash(route)default:return err}
}func add(nd *node, route string, item interface{}) error {if len(route) == 0 {if nd.item != nil {return errDupItem}nd.item = itemreturn nil}// 继续判断,看看是不是有多个 /if route[0] == slash {return errDupSlash}for i := range route {// 判断是不是 /,目的就是去处两个 / 之间的内容if route[i] != slash {continue}token := route[:i]// 看看有没有子节点,如果有子节点,就在子节点下面继续添加children := nd.getChildren(token)if child, ok := children[token]; ok {if child != nil {return add(child, route[i+1:], item)}return errInvalidState}// 没有子节点,那么新建一个child := newNode(nil)children[token] = childreturn add(child, route[i+1:], item)}children := nd.getChildren(route)if child, ok := children[route]; ok {if child.item != nil {return errDupItem}child.item = item} else {children[route] = newNode(item)}return nil
}
主要部分代码都已经加了注释,其实这个过程就是树的构建,如果读过之前那篇文章,那这里还是比较好理解的。
路由查找
先来看一段 match
代码:
func match(pat, token string) innerResult {if pat[0] == colon {return innerResult{key: pat[1:],value: token,named: true,found: true,}}return innerResult{found: pat == token,}
}
这里有两个参数:
pat
:路由树中存储的路由token
:实际请求的路由,可能包含参数值
还是刚才的例子 /api/:user
,如果是 api
,没有以 :
开头,那就不会走 if
逻辑。
接下来匹配 :user
部分,如果实际请求的 url 是 /api/zhangsan
,那么会将 user
作为 key
,zhangsan
作为 value
保存到结果中。
下面是搜索查找代码:
// Search searches item that associates with given route.
func (t *Tree) Search(route string) (Result, bool) {// 第一步先判断是不是 / 开头if len(route) == 0 || route[0] != slash {return NotFound, false}var result Resultok := t.next(t.root, route[1:], &result)return result, ok
}func (t *Tree) next(n *node, route string, result *Result) bool {if len(route) == 0 && n.item != nil {result.Item = n.itemreturn true}for i := range route {// 和 add 里同样的提取逻辑if route[i] != slash {continue}token := route[:i]return n.forEach(func(k string, v *node) bool {r := match(k, token)if !r.found || !t.next(v, route[i+1:], result) {return false}// 如果 url 中有参数,会把键值对保存到结果中if r.named {addParam(result, r.key, r.value)}return true})}return n.forEach(func(k string, v *node) bool {if r := match(k, route); r.found && v.item != nil {result.Item = v.itemif r.named {addParam(result, r.key, r.value)}return true}return false})
}
以上就是路由管理的大部分代码,整个文件也就 200 多行,逻辑也并不复杂,通读之后还是很有收获的。
大家如果感兴趣的话,可以找到项目更详细地阅读。也可以关注我,接下来还会分析其他模块的源码。
以上就是本文的全部内容,如果觉得还不错的话欢迎点赞,转发和关注,感谢支持。
推荐阅读:
- 使用 Go 语言实现二叉搜索树
- HTTP Router 算法演进
相关文章:

go-zero 是如何做路由管理的?
原文链接: go-zero 是如何做路由管理的? go-zero 是一个微服务框架,包含了 web 和 rpc 两大部分。 而对于 web 框架来说,路由管理是必不可少的一部分,那么本文就来探讨一下 go-zero 的路由管理是怎么做的,…...

Springboot集成ip2region离线IP地名映射-修订版
title: Springboot集成ip2region离线IP地名映射 date: 2020-12-16 11:15:34 categories: springboot description: Springboot集成ip2region离线IP地名映射 1. 背景2. 集成 2.1. 步骤2.2. 样例2.3. 响应实例DataBlock2.4. 响应实例RegionAddress 3. 打开浏览器4. 源码地址&…...

智能驾驶系列报告之一:智能驾驶 ChatGPT时刻有望来临
原创 | 文 BFT机器人 L3 功能加速落地,政策标准有望明确 L2 发展日益成熟,L3 功能加速落地。根据市场监管总局发布的《汽车驾驶自动化分级》与 SAE发布的自动驾驶分级标准,自动驾驶主要分为 6 个级别(0 级到 5 级,L0 …...

设计HTML5文档结构
定义清晰、一致的文档结构不仅方便后期维护和拓展,同时也大大降低了CSS和JavaScript的应用难度。为了提高搜索引擎的检索率,适应智能化处理,设计符合语义的结构显得很重要。 1、头部结构 在HTML文档的头部区域,存储着各种网页元…...
vue echarts中按钮点击后修改值 watch数据变化后刷新图表
1 点击按钮 {feature: {myBtn1: {show: true,title: 反转Y轴,showTitle: true,icon: path://M512 0A512 512 0 1 0 512 1024A512 512 0 0 0 512 0M320 320V192h384v128zM128 416V288h256v128zM320 704V576h384v128zM128 800V672h256v128z,onclick: () > {dataSetting.rever…...

React antd tree树组件 - 父子节点没有自动关联情况下 - 显示半选、全选状态以及实现父子节点互动
实现的效果图如下: 如Ant Design Vue 中所示,并没有提供获取半选节点的方法,当设置checked和checkStrictly时,父子节点也不再自动关联了 前提:从后端可以获取的数据分别是完整的树型数据、所有选中的节点数据&#…...
优漫动游 大厂需要什么样的ui设计师呢?
通常来说大公司UI设计的流程主要是这样的:创意-头脑风暴-策划方案-交互设计&评审-美术设计&评审-开发实施,不过实际上大多数公司都有自己的一套流程,源于公司的基因、公司组织体系、公司领导风格。一起了解大厂需要什么样的ui设计师呢…...

TiDB Bot:用 Generative AI 构建企业专属的用户助手机器人
本文介绍了 PingCAP 是如何用 Generative AI 构建一个使用企业专属知识库的用户助手机器人。除了使用业界常用的基于知识库的回答方法外,还尝试使用模型在 few shot 方法下判断毒性。 最终,该机器人在用户使用后,点踩的比例低于 5%࿰…...

uniapp 使用canvas画海报(微信小程序)
效果展示: 项目要求:点击分享绘制海报,并实现分享到好友,朋友圈,并保存 先实现绘制海报 <view class"data_item" v-for"(item,index) in dataList" :key"index"click"goDet…...
TiDB 应急运维脚本,更加方便的管理TiDB集群
TiDB 应急运维脚本,更加方便的管理TiDB集群 使用方法 使用方法:[tidblocalhost ~]$ which tiup ~/.tiup/bin/tiup编辑脚本,MYSQL_PASSWD 和 PORT 根据实际替换 [tidblocalhost ~]$ vi ~/.tiup/bin/ti#version 1.1 #author guanguanglei ##…...

第二部分:AOP
一、AOP简介 AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。 AOP是OOP(面向对象编程)的进阶版。 作用:在不改变原始设计的基础上为其进行功能增强。 spring理念&#x…...

分享一组天气组件
先看效果: CSS部分代码(查看更多): <style>:root {--bg-color: #E9F5FA;--day-text-color: #4DB0D3;/* 多云 */--cloudy-background: #4DB0D3;--cloudy-temperature: #E6DF95;--cloudy-content: #D3EBF4;/* 晴 */--sunny-b…...
微服务中RestTemplate访问其他服务返回值转换问题
背景: 接收一个springcloud项目,UI模块访问其他服务的接口,返回数据统一都是使用fastjson进行转换,但是新开发了几个新模块之后发现fastjson很多bug(各种内存溢出),但是很多地方已经重度依赖fa…...

Centos7.9上(离线)安装Gitlab
1、下载Gitlab的rpm安装包Index of /gitlab-ce/yum/el7/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 2、安装rpm -i gitlab-ce-10.0.0-ce.0.el7.x86_64.rpm,如果依赖缺失,yum安装即可 3、vi /etc/gitlab/gitlab.rb 配置external_url&…...

pikachu中RCE出现乱码的解决的方案
exec “ping” 输入127.0.0.1 这种乱码的解决办法就是在pikachu/vul/rce/rce_ping.php目录里面的第18行代码 header("Content-type:text/html; charsetgbk");的注释打开即可。 BUT但是吧!又出现了其他的乱码!但是搞完这个再把它给注释掉就行了…...
airflow是什么
Airflow 简介 Airflow是一个基于有向无环图(DAG)的可编程、调度和监控的工作流平台,它可以定义一组有依赖的任务,按照依赖依次执行。airflow提供了丰富的命令行工具用于系统管控,而其web管理界面同样也可以方便的管控调度任务,并…...
训练用于序列分类任务的 RoBERTa 模型的适配器
介绍 NLP当前的趋势包括下载和微调具有数百万甚至数十亿参数的预训练模型。然而,存储和共享如此大的训练模型非常耗时、缓慢且昂贵。这些限制阻碍了 RoBERTa 模型开发更多用途和适应性更强的 NLP 技术,该模型可以从多个任务中学习并针对多个任务进行学习;在本文中,我们将重…...

Linux之awk判断和循环
echo zhaoy 70 72 74 76 74 72 >> score.txt echo wangl 70 81 84 82 90 88 >> score.txt echo qiane 60 62 64 66 65 62 >> score.txt echo sunw 80 83 84 85 84 85 >> score.txt echo lixi 96 80 90 95 89 87 >> score.txt把下边的内容写入到s…...

Django入门
Day1 django环境安装 创建虚拟环境 # step1 创建虚拟环境 python3 -m venv datawhale_django # step2 mac进入虚拟环境 source ./datawhale_django/bin/activate # step3 退出虚拟环境 deactivate安装包 pip3 install django pip3 install djangorestframework pip3 …...

uniapp 格式化时间刚刚,几分钟前,几小时前,几天前…
效果如图: 根目录下新建utils文件夹,文件夹下新增js文件,文件内容: export const filters {dateTimeSub(data) {if (data undefined) {return;}// 传进来的data必须是日期格式,不能是时间戳//将字符串转换成时间格式…...
[特殊字符] 智能合约中的数据是如何在区块链中保持一致的?
🧠 智能合约中的数据是如何在区块链中保持一致的? 为什么所有区块链节点都能得出相同结果?合约调用这么复杂,状态真能保持一致吗?本篇带你从底层视角理解“状态一致性”的真相。 一、智能合约的数据存储在哪里…...

调用支付宝接口响应40004 SYSTEM_ERROR问题排查
在对接支付宝API的时候,遇到了一些问题,记录一下排查过程。 Body:{"datadigital_fincloud_generalsaas_face_certify_initialize_response":{"msg":"Business Failed","code":"40004","sub_msg…...
逻辑回归:给不确定性划界的分类大师
想象你是一名医生。面对患者的检查报告(肿瘤大小、血液指标),你需要做出一个**决定性判断**:恶性还是良性?这种“非黑即白”的抉择,正是**逻辑回归(Logistic Regression)** 的战场&a…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决
Spring Cloud Gateway 中自定义验证码接口返回 404 的排查与解决 问题背景 在一个基于 Spring Cloud Gateway WebFlux 构建的微服务项目中,新增了一个本地验证码接口 /code,使用函数式路由(RouterFunction)和 Hutool 的 Circle…...

使用LangGraph和LangSmith构建多智能体人工智能系统
现在,通过组合几个较小的子智能体来创建一个强大的人工智能智能体正成为一种趋势。但这也带来了一些挑战,比如减少幻觉、管理对话流程、在测试期间留意智能体的工作方式、允许人工介入以及评估其性能。你需要进行大量的反复试验。 在这篇博客〔原作者&a…...

如何应对敏捷转型中的团队阻力
应对敏捷转型中的团队阻力需要明确沟通敏捷转型目的、提升团队参与感、提供充分的培训与支持、逐步推进敏捷实践、建立清晰的奖励和反馈机制。其中,明确沟通敏捷转型目的尤为关键,团队成员只有清晰理解转型背后的原因和利益,才能降低对变化的…...

嵌入式学习之系统编程(九)OSI模型、TCP/IP模型、UDP协议网络相关编程(6.3)
目录 一、网络编程--OSI模型 二、网络编程--TCP/IP模型 三、网络接口 四、UDP网络相关编程及主要函数 编辑编辑 UDP的特征 socke函数 bind函数 recvfrom函数(接收函数) sendto函数(发送函数) 五、网络编程之 UDP 用…...
如何通过git命令查看项目连接的仓库地址?
要通过 Git 命令查看项目连接的仓库地址,您可以使用以下几种方法: 1. 查看所有远程仓库地址 使用 git remote -v 命令,它会显示项目中配置的所有远程仓库及其对应的 URL: git remote -v输出示例: origin https://…...