go-zero 是如何做路由管理的?

news2024/11/15 11:31:57

原文链接: 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.go

const (
    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 = item
        return 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] = child
        return 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 Result
    ok := 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.item
        return 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.item
            if r.named {
                addParam(result, r.key, r.value)
            }

            return true
        }

        return false
    })
}

以上就是路由管理的大部分代码,整个文件也就 200 多行,逻辑也并不复杂,通读之后还是很有收获的。

大家如果感兴趣的话,可以找到项目更详细地阅读。也可以关注我,接下来还会分析其他模块的源码。

以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。


推荐阅读:

  • 使用 Go 语言实现二叉搜索树
  • HTTP Router 算法演进

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

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

相关文章

速卖通,国际站店铺想要增加曝光,提升销量,测评补单有效果吗?

作为一个卖家,成功运营速卖通店铺需要一系列的策略和技巧 1.借助平台的力量对于成长期的店铺来说,平台本身是最大的流量来源。如何从平台那儿获取更多的支持则成为这个阶段最重要的难题。以速卖通为例,经过反复测试,平台给普通卖家…

LeetCode练习习题集【4月 - 7 月】

LEETCODE习题集【4月-7月总结】 简单 数组部分 1.重复数 题目: 在一个长度u为 n 的数组 nums 里的所有数字都在 0~n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中…

蛋糕小程序商店制作攻略教程分享

想要开发一个蛋糕小程序商店,可以通过以下步骤进行操作: 1.首先,我们需要注册登录账号,进入操作后台。找到并点击【商城】中的【去管理】进入商城的后台管理页面。然后再点击【小程序商城】模块中的【去装修】进入小程序商城的制作…

【torch.nn.PixelShuffle】和 【torch.nn.UnpixelShuffle】

文章目录 torch.nn.PixelShuffle直观解释官方文档 torch.nn.PixelUnshuffle直观解释官方文档 torch.nn.PixelShuffle 直观解释 PixelShuffle是一种上采样方法,它将形状为 ( ∗ , C r 2 , H , W ) (∗, C\times r^2, H, W) (∗,Cr2,H,W)的张量重新排列转换为形状为…

网络安全 Day28-运维安全项目-加密隧道

运维安全项目-加密隧道 1. 加密隧道服务概述2. openVPN应用场景3. 虚拟机环境准备3.0 准备知识3.1 添加网卡![请添加图片描述](https://img-blog.csdnimg.cn/f155ca2804d84118b89a69da3688911e.png)3.2 配置内网(LAN区段)3.3 虚拟机选择LAN区段3.4 书写eth1网卡配置…

力扣hot100刷题记录

二刷hot100&#xff0c;坚持每天打卡&#xff01;&#xff01;&#xff01; 1. 两数之和 // 先求差&#xff0c;再查哈希表 public int[] twoSum(int[] nums, int target) {Map<Integer,Integer> map new HashMap<>();for(int i 0;i<nums.length;i){int key …

UE Mesh Generation and Editing at Runtime

UE Mesh Generation and Editing at Runtime 虚幻运行时和编辑器下生成和编辑 网格体。 UE Mesh 虚幻中常用的三种网格体 UProceduralMeshComponent 程序化网格体 UStaticMeshComponent 静态网格体 USimpleDynamicMeshComponent 动态网格体 借用他人总结的&#xff0c;UE4…

设计一个“完美“的测试用例,用户登录模块实例...

前言 好的测试用例一定是一个完备的集合&#xff0c;它能够覆盖所有等价类以及各种边界值&#xff0c;而跟能否发现缺陷无关 好的测试用例必须具备哪些特征 整体完备性&#xff1a;一定是一个完备的整体&#xff0c;是有效测试用例组成的集合&#xff0c;能够完全覆盖测试需…

在vue中Antv G2 折线图如何添加点击事件获取折线上点的值

在项目中有个需求是点击折线图的点&#xff0c;获取当前点的信息&#xff0c;其它图形都可以参考相关的API获取到&#xff0c;但area做的折线图怎么都获取不到点击的信息&#xff0c;只能获取全部的信息&#xff0c;最终解决如下&#xff1a; 实现思路 用户的鼠标在折线图上移…

高中教师能去美国做访问学者吗?

美国作为世界上高等教育水平较高的国家之一&#xff0c;吸引了众多学者前往交流学习。那么高中教师是否能够成为美国访问学者&#xff0c;这是当然的&#xff0c;高中老师是可以出国访学的&#xff0c;但是出国做访问学者会涉及到多方面的因素。 首先&#xff0c;教师个人的学术…

带你Debug SpringApplication.run(MainApp.class, args) 看看SpringBoot 如何启动Tomcat

&#x1f600;前言 本篇博文是关于SpringBoot 如何启动Tomcat的笔记&#xff0c;希望能够让你到SpringBoot印象深刻&#x1f60a; &#x1f3e0;个人主页&#xff1a;晨犀主页 &#x1f9d1;个人简介&#xff1a;大家好&#xff0c;我是晨犀&#xff0c;希望我的文章可以帮助到…

2023深圳杯A题完整代码模型

已更新深圳杯A题全部版本&#xff0c;文末获取&#xff01; 摘要 现代社会&#xff0c;随着生活方式的变化和工作压力的增大&#xff0c;慢性非传染性疾病日益成为威胁公众健康的主要问题。心脑血管疾病、糖尿病、恶性肿瘤及慢性阻塞性肺病等慢性病的发病率呈现出上升趋势。为…

Linux 安装部署Seata

标题&#xff1a;在Linux上安装部署Seata分布式事务解决方案 导语&#xff1a; Seata是一个开源的分布式事务解决方案&#xff0c;旨在解决分布式环境下的事务一致性问题。本文将为您介绍如何在Linux操作系统上安装和部署Seata&#xff0c;为您的分布式应用添加强大的事务支持。…

【【萌新的STM32学习-7】】

萌新的STM32学习-7 MAP 文件是MDK代码编译之后&#xff0c;产生的集程序&#xff0c;数据及IO 空间的一种映射列表文件 map 文件是编译器链接时生成的一个文件&#xff0c;它主要包含了交叉链接信息。通过.map 文 件&#xff0c;我们可以知道整个工程的函数调用关系、FLASH 和 …

ThingJS开发使用感受

封面来源于网络。 一、前言 1. 背景 出于为了实现有关厂区的数字孪生项目&#xff0c;断断续续使用ThingJS平台开发一年左右&#xff0c;做一个使用感受的总结。 2. 业务场景 开发一个基于厂区的数字孪生项目&#xff0c;基于ThingJS低代码开发的页面分为div3d、div2d结构&am…

【深度学习笔记】深度学习框架

本专栏是网易云课堂人工智能课程《神经网络与深度学习》的学习笔记&#xff0c;视频由网易云课堂与 deeplearning.ai 联合出品&#xff0c;主讲人是吴恩达 Andrew Ng 教授。感兴趣的网友可以观看网易云课堂的视频进行深入学习&#xff0c;视频的链接如下&#xff1a; 神经网络和…

【源码编译并安装RocketMQ Dashboard】

【源码编译并安装RocketMQ Dashboard】 一、环境说明二、源码编译并执行三、小结 一、环境说明 安装环境&#xff1a;虚拟机VMWare Centos7.6 Maven3.6.3 JDK1.8已经安装了RocketMQ-5.1.3 单Master集群&#xff0c;且使用Local模式部署&#xff0c;即Broker和Proxy同进程部署…

【Java】CAS数据交换流程

CAS的全称是&#xff1a; Compare And Swap(比较再交换)&#xff0c;它体现的一种乐观锁的思想&#xff0c;在无锁情况下保证线程操作共享数据的原子性。 CAS数据交换流程&#xff1a; 此时线程A和线程B都从主内存中拷贝了一份a100的共享变量到自己的工作内存中 线程A操作了变…

手势识别rtos小车(1)----手部识别

1.安装mediapipe库和cv2库 pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simplepip install Mediapipe0.8.9.1 -i https://pypi.tuna.tsinghua.edu.cn/simple some-package 这里我主要还是弄明白了这个pycharm编辑器和项目之间的关系&#xff0c;我在这里…

联盟CPS聚合联盟聚推客推广项目赚钱吗?揭秘有人月入10万+

大家好&#xff0c;我是巧匠&#xff01;最近因为忙碌于项目&#xff0c;都没来得及发布新的文章。不过现在&#xff0c;我给大家带来了一个新的项目玩法——那就是我们常说的淘客CPS系统。相信大家对“聚推客联盟”这个系统都有所了解。这个系统的模式真的很棒&#xff0c;让我…