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

打印 上一主题 下一主题

主题 895|帖子 895|积分 2685

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

路由管理方案有很多种,具体应该如何选择,应该根据使用场景,以及实现的难易程度做综合分析,下面介绍常见的三种方案。
注意这里只是做一个简单的概括性对比,更加详细的内容可以看这篇文章:HTTP Router 算法演进
标准库方案

最简单的方案就是直接使用 map[string]func() 作为路由的数据结构,键为具体的路由,值为具体的处理方法。
  1. // 路由管理数据结构
  2. type ServeMux struct {
  3.     mu    sync.RWMutex          // 对象操作读写锁
  4.     m     map[string]muxEntry   // 存储路由映射关系
  5. }
复制代码
这种方案优点就是实现简单,性能较高;缺点也很明显,占用内存更高,更重要的是不够灵活。
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 语言实现二叉搜索树
节点定义

先看一下节点定义:
  1. // core/search/tree.go
  2. const (
  3.     colon = ':'
  4.     slash = '/'
  5. )
  6. type (
  7.     // 节点
  8.     node struct {
  9.         item     interface{}
  10.         children [2]map[string]*node
  11.     }
  12.     // A Tree is a search tree.
  13.     Tree struct {
  14.         root *node
  15.     }
  16. )
复制代码
重点说一下 children,它是一个包含两个元素的数组,元素 0 存正常路由键,元素 1 存以 : 开头的路由键,这些是 url 中的变量,到时候需要替换成实际值。
举一个例子,有这样一个路由 /api/:user,那么 api 会存在 children[0],user 会存在 children[1]。
具体可以看看这段代码:
  1. func (nd *node) getChildren(route string) map[string]*node {
  2.     // 判断路由是不是以 : 开头
  3.     if len(route) > 0 && route[0] == colon {
  4.         return nd.children[1]
  5.     }
  6.     return nd.children[0]
  7. }
复制代码
路由添加
  1. // Add adds item to associate with route.
  2. func (t *Tree) Add(route string, item interface{}) error {
  3.     // 需要路由以 / 开头
  4.     if len(route) == 0 || route[0] != slash {
  5.         return errNotFromRoot
  6.     }
  7.     if item == nil {
  8.         return errEmptyItem
  9.     }
  10.     // 把去掉 / 的路由作为参数传入
  11.     err := add(t.root, route[1:], item)
  12.     switch err {
  13.     case errDupItem:
  14.         return duplicatedItem(route)
  15.     case errDupSlash:
  16.         return duplicatedSlash(route)
  17.     default:
  18.         return err
  19.     }
  20. }
  21. func add(nd *node, route string, item interface{}) error {
  22.     if len(route) == 0 {
  23.         if nd.item != nil {
  24.             return errDupItem
  25.         }
  26.         nd.item = item
  27.         return nil
  28.     }
  29.     // 继续判断,看看是不是有多个 /
  30.     if route[0] == slash {
  31.         return errDupSlash
  32.     }
  33.     for i := range route {
  34.         // 判断是不是 /,目的就是去处两个 / 之间的内容
  35.         if route[i] != slash {
  36.             continue
  37.         }
  38.         token := route[:i]
  39.         
  40.         // 看看有没有子节点,如果有子节点,就在子节点下面继续添加
  41.         children := nd.getChildren(token)
  42.         if child, ok := children[token]; ok {
  43.             if child != nil {
  44.                 return add(child, route[i+1:], item)
  45.             }
  46.             return errInvalidState
  47.         }
  48.         // 没有子节点,那么新建一个
  49.         child := newNode(nil)
  50.         children[token] = child
  51.         return add(child, route[i+1:], item)
  52.     }
  53.     children := nd.getChildren(route)
  54.     if child, ok := children[route]; ok {
  55.         if child.item != nil {
  56.             return errDupItem
  57.         }
  58.         child.item = item
  59.     } else {
  60.         children[route] = newNode(item)
  61.     }
  62.     return nil
  63. }
复制代码
主要部分代码都已经加了注释,其实这个过程就是树的构建,如果读过之前那篇文章,那这里还是比较好理解的。
路由查找

先来看一段 match 代码:
  1. func match(pat, token string) innerResult {
  2.     if pat[0] == colon {
  3.         return innerResult{
  4.             key:   pat[1:],
  5.             value: token,
  6.             named: true,
  7.             found: true,
  8.         }
  9.     }
  10.     return innerResult{
  11.         found: pat == token,
  12.     }
  13. }
复制代码
这里有两个参数:

  • pat:路由树中存储的路由
  • token:实际请求的路由,可能包含参数值
还是刚才的例子 /api/:user,如果是 api,没有以 : 开头,那就不会走 if 逻辑。
接下来匹配 :user 部分,如果实际请求的 url 是 /api/zhangsan,那么会将 user 作为 key,zhangsan 作为 value 保存到结果中。
下面是搜索查找代码:
  1. // Search searches item that associates with given route.
  2. func (t *Tree) Search(route string) (Result, bool) {
  3.     // 第一步先判断是不是 / 开头
  4.     if len(route) == 0 || route[0] != slash {
  5.         return NotFound, false
  6.     }
  7.     var result Result
  8.     ok := t.next(t.root, route[1:], &result)
  9.     return result, ok
  10. }
  11. func (t *Tree) next(n *node, route string, result *Result) bool {
  12.     if len(route) == 0 && n.item != nil {
  13.         result.Item = n.item
  14.         return true
  15.     }
  16.     for i := range route {
  17.         // 和 add 里同样的提取逻辑
  18.         if route[i] != slash {
  19.             continue
  20.         }
  21.         token := route[:i]
  22.         return n.forEach(func(k string, v *node) bool {
  23.             r := match(k, token)
  24.             if !r.found || !t.next(v, route[i+1:], result) {
  25.                 return false
  26.             }
  27.             // 如果 url 中有参数,会把键值对保存到结果中
  28.             if r.named {
  29.                 addParam(result, r.key, r.value)
  30.             }
  31.             return true
  32.         })
  33.     }
  34.     return n.forEach(func(k string, v *node) bool {
  35.         if r := match(k, route); r.found && v.item != nil {
  36.             result.Item = v.item
  37.             if r.named {
  38.                 addParam(result, r.key, r.value)
  39.             }
  40.             return true
  41.         }
  42.         return false
  43.     })
  44. }
复制代码
以上就是路由管理的大部分代码,整个文件也就 200 多行,逻辑也并不复杂,通读之后还是很有收获的。
大家如果感兴趣的话,可以找到项目更详细地阅读。也可以关注我,接下来还会分析其他模块的源码。
以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。
推荐阅读:

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

玛卡巴卡的卡巴卡玛

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表