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 开发项目时,定义路由需要遵守如下规则:

  1. 路由必须以 / 开头
  2. 路由节点必须以 / 分隔
  3. 路由节点中可以包含 :,但是 : 必须是路由节点的第一个字符,: 后面的节点值必须要在结请求体中有 path tag 声明,用于接收路由参数
  4. 路由节点可以包含字母、数字、下划线、中划线

接下来就让我们深入到源码层面,相信看过源码之后,你就会更懂这些规则的意义了。

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 作为 keyzhangsan 作为 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 算法演进

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

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

相关文章

hbuilder的获取头像以及位置

条件编译 // #ifndef VUE3 import Vue from vue import ./uni.promisify.adaptor Vue.config.productionTip false App.mpType app // 初始化vue应用 const app new Vue({...App }) // 挂载vue应用 app.$mount() // #endif// #ifdef VUE3 import { createSSRApp } from vue…

uniapp调查问卷评价功能

我本来用的是uniapp官方提供的组件uni-rate组件&#xff0c;但修改成我想要的样式有点麻烦&#xff0c;于是我就自己手写一个&#xff0c;比用组件简单一点&#xff1b; dom结构 <text class"formTit must">请您对本次活动进行评价</text> <view cl…

【Axure动态面板】利用动态面板实现树形菜单的制作

利用动态面板&#xff0c;简单制作高保真的树形菜单。 一、先看效果 https://1poppu.axshare.com 二、实现思路 1、菜单无非就是收缩和展开&#xff0c;动态面板有个非常好的属性&#xff1a;fit to content&#xff0c;这个属性的含义是&#xff1a;面板的大小可以根据内容多少…

Java课题笔记~ 使用 Spring 的事务注解管理事务(掌握)

通过Transactional 注解方式&#xff0c;可将事务织入到相应 public 方法中&#xff0c;实现事务管理。 Transactional 的所有可选属性如下所示&#xff1a; propagation&#xff1a;用于设置事务传播属性。该属性类型为 Propagation 枚举&#xff0c; 默认值为 Propagation.R…

2. 软件需求 面向对象分析

目录 1. 软件需求 1.1 需求分类 1.2 需求获取 1.3 需求分析 2. 面向对象分析&#xff08;OOA&#xff09; 2.1 统一建模语言 UML 2.2 用例模型 2.2.1 用例图的元素 2.2.2 识别参与者 2.2.3 合并需求获得用例 2.2.4 细化用例描述 2.3 分析模型 2.3.1 定义概念类 …

【MySQL系列】表约束的学习

「前言」文章内容大致是MySQL的表的约束。 「归属专栏」MySQL 「主页链接」个人主页 「笔者」枫叶先生(fy) 目录 一、MySQL表的约束1.1 空属性1.2 默认值&#xff08;default&#xff09;1.3 列描述&#xff08;comment&#xff09;1.4 zerofill1.5 主键&#xff08;primary ke…

element+vue 之动态form

1.页面部分 <div v-for"(item,index) in formList" :key"index"><el-col :span"6" v-if"item.inputType0"><el-form-item :label"item.conditionName" :prop"item.conditionCode":rules"{req…

我的2048创作纪念日

目录 一、来龙去脉二、2048天前的今天三、2048天的收获四、2048天后的今天五、祝福 一、来龙去脉 今天是 2023 年 8 月 9 日&#xff0c;在咖啡馆浏览我的 CSDN 主页时&#xff0c;看到官方发来了一则私信&#xff1a; 二、2048天前的今天 提示&#xff1a;可以和大家分享最初…

使用docker 搭建nginx + tomcat 集群

创建3个Tomcat容器&#xff0c;端口分别映射到 8080,8081,8082&#xff0c;使用数据卷挂载&#xff0c;分别将宿主机目录下的 /opt/module/docker/tomcat3/ROOT1/&#xff0c;/opt/module/docker/tomcat3/ROOT2/&#xff0c;/opt/module/docker/tomcat3/ROOT2/ 挂载到 容器内部…

Java从入门到精通(四)· 面向对象

Java从入门到精通&#xff08;四&#xff09; 面向对象 一 静态关键字&#xff08;static&#xff09; static表示静态关键字&#xff0c;可以用来修饰成员变量和成员方法。 被 static修饰的成员变量&#xff0c;称之为静态成员变量或者类变量&#xff0c;同理可得&#xff…

【个人记录】CentOS7 编译安装最新版本Git

说明 使用yum install git安装的git版本是1.8&#xff0c;并不是最新版本&#xff0c;使用gitlab-runner托管时候会拉项目失败&#xff0c;这里使用编译源码方式安装最新版本的git。 基础环境安装 echo "nameserver 8.8.8.8" >> /etc/resolv.conf curl -o /…

ArmSoM-W3之RK3588安装Qt+opencv+采集摄像头画面

1. 简介 场景&#xff1a;在RK3588上做qt开发工作 RK3588安装Qtopencv采集摄像头画面 2. 环境介绍 这里使用了OpenCV所带的库函数捕获摄像头的视频图像。 硬件环境&#xff1a; ArmSoM-RK3588开发板、&#xff08;MIPI-DSI&#xff09;摄像头 软件版本&#xff1a; OS&…