首页 > 其他分享 >go-zero 是如何做路由管理的?

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

时间:2023-08-09 22:34:39浏览次数:56  
标签:return route Tree item zero go children 路由

原文链接: 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 多行,逻辑也并不复杂,通读之后还是很有收获的。

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

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


推荐阅读:

标签:return,route,Tree,item,zero,go,children,路由
From: https://blog.51cto.com/alwaysbeta/7026491

相关文章

  • Go语言中省略号用法大全
    Go语言中的ellipsis(即三个连续的点...)确实有几种用途。下面是这些用途的总结:定义变参函数(VariadicFunctions):Ellipsis用于函数定义时,表明该函数可以接受任意数量的参数。这些参数会被当作一个slice来处理。例如:funcsum(nums...int)int{total:=0for_,......
  • go-zero 是如何做路由管理的?
    原文链接:go-zero是如何做路由管理的?go-zero是一个微服务框架,包含了web和rpc两大部分。而对于web框架来说,路由管理是必不可少的一部分,那么本文就来探讨一下go-zero的路由管理是怎么做的,具体采用了哪种技术方案。路由管理方案路由管理方案有很多种,具体应该如何选择,应......
  • GO八股
    Go股大全:flag库了解吗?有什么陷阱?flag是Go官方提供的标准库,flag包实现了命令行的解析,flag使得开发命令行工具更为简单;陷阱一:当我们把flag放置在cli应用的最后面时,需要小心参数传递的顺序,flag包的命令行参数的解析逻辑是:当碰到第一个非flag参数时,便停止解析,所以如果传入......
  • Godot无法响应鼠标点击等输入事件时,检查这些内容
    注:本文以Godot4.0为基准,可能其他版本也能参考。这是我用C#写项目时发现的,可能和gdscript使用者遇到的问题有一定区别。如果你用Godot制作的游戏无法响应鼠标点击等输入事件,请检查下面这几种可能性,说不定能解决你的问题:1绑定gui_input信号的节点是否是Control?绑定p......
  • vue+django跨域问题
    解决办法:MIDDLEWARE=['corsheaders.middleware.CorsMiddleware','yshop.middleware.AuthorizeMiddleware','django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware�......
  • Python基础day63 Django分页和cookie、session
    推导分页的原理分页:当我们要展示的数据特别多的时候,一页展示不完,这个时候我们需要把要展示的数据分成多页展示分页中需要的几个参数:1.总数据有多少条2.每页展示多少条数据(自己规定的20)3.一共展示多少页4.总页数=总数据量/每页展示多少条数据5.当前第几页(前端传过去......
  • study go of First day
    一、语言标准1、go语言标识符基本和c语言规则一致:1).由数字、字母、下划线(_)组成2).数字不能在开头3).区分大小写————————————————————————————————————————————————————————————————————————————......
  • HCIP学习(1)——基础路由
    一.路由基础当路由器收到一个IP数据包,路由器会根据数据包的目的IP地址查找FIB(ForwardingInformationBase,转发信息库)表,找到“最匹配”的路由条目后,将数据包根据路由条目所指示的出接口或下一跳转发出去。路由器获取路由的方式有三种,分别是动态路由(例如OSPF)、静态路由(Static)和......
  • 简易的学生管理系统(go语言)
    简易的学生管理系统(go语言)首先,你需要安装github.com/go-sql-driver/mysql这个包来操作MySQL数据库。你可以使用以下命令来安装它:gogetgithub.com/go-sql-driver/mysqlpackagemainimport( "database/sql" "fmt" "os" "bufio" "strings" _"......
  • 使用golang解决LeetCode热题Hot100(1-10)
    使用golang解决LeetCode热题Hot1001.两数之和https://leetcode.cn/problems/two-sum/题目给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。你可以假设每种输入只会对应一个答案。但是,数组中同一个......