全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-18 10 分钟 ✍️ juanwangdev

Gin路由树构建与查找算法

Gin使用压缩前缀树(Radix Tree)实现高效路由,理解其算法有助于优化路由设计。

Radix树基础

节点结构

Go
type node struct {
    path      string      // 节点路径
    indices   string      // 子节点首字符索引
    children  []*node     // 子节点
    handlers  HandlersChain // 处理函数
    priority  uint32      // 优先级(访问频率)
    nType     uint8       // 节点类型
    maxParams uint8       // 最大参数数量
    wildChild bool        // 是否有通配子节点
}

// 节点类型常量
const (
    static nodeType = iota  // 静态节点
    root                    // 根节点
    param                   // 参数节点 :param
    catchAll                // 通配节点 *filepath
)

树结构示意

Go
注册路由:
GET /users
GET /users/:id
GET /users/profile
GET /articles/:id

Radix树结构:
root
└── users (handlers)
    ├── /profile (handlers)
    └── /:id (handlers)

root
└── articles/
    └── :id (handlers)

路由添加算法

核心addRoute方法

Go
func (n *node) addRoute(path string, handlers HandlersChain) {
    fullPath := path

    // 空树直接设置
    if n.path == "" && n.indices == "" {
        n.insertChild(path, fullPath, handlers)
        return
    }

Walk:
    for {
        // 找最长公共前缀
        i := longestCommonPrefix(path, n.path)

        // 需要分割节点
        if i < len(n.path) {
            child := node{
                path:     n.path[i:],
                indices:  n.indices,
                children: n.children,
                handlers: n.handlers,
                priority: n.priority,
            }
            n.path = path[:i]
            n.indices = string([]byte{child.path[0]})
            n.children = []*node{&child}
            n.handlers = nil
        }

        // 添加新路径
        if i < len(path) {
            path = path[i:]

            // 查找匹配子节点
            for i, idx := range []byte(n.indices) {
                if idx == path[0] {
                    n = n.children[i]
                    continue Walk
                }
            }

            // 创建新子节点
            n.insertChild(path, fullPath, handlers)
            return
        }

        // 路径已存在
        n.handlers = handlers
        return
    }
}

节点分割示例

Go
已有路由: /hello
新增路由: /help

步骤1: 找公共前缀 "hel"
步骤2: 分割节点
  /hello → /hel → llo (子节点)
步骤3: 添加新分支
  /hel → llo
       → p

最终树:
root
└── hel
    ├── lo (handlers for /hello)
    └── p  (handlers for /help)

通配符处理

参数节点插入

text
func (n *node) insertChild(path, fullPath string, handlers HandlersChain) {
    for {
        // 查找通配符
        wildcard, i, valid := findWildcard(path)

        if !valid {
            panic("invalid wildcard")
        }

        if wildcard == "" {
            break
        }

        // 参数类型 :param
        if wildcard[0] == ':' {
            // 创建参数节点
            n.wildChild = true
            child := &node{
                nType: param,
                path:  wildcard,
            }
            n.children = append(n.children, child)
            n = child
        }

        // 通配类型 *filepath
        if wildcard[0] == '*' {
            child := &node{
                nType: catchAll,
                path:  wildcard,
            }
            n.children = append(n.children, child)
            n = child
        }

        path = path[i+1:]
    }

    n.handlers = handlers
}

路由查找算法

getValue核心方法

text
func (n *node) getValue(path string, params *Params, unescape bool) (value nodeValue) {
    value = nodeValue{}

Walk:
    for {
        prefix := n.path

        // 完全匹配
        if path == prefix {
            value.handlers = n.handlers
            return
        }

        // 前缀匹配
        if len(path) > len(prefix) && path[:len(prefix)] == prefix {
            path = path[len(prefix):]

            // 静态子节点匹配
            if !n.wildChild {
                idxc := path[0]
                for i, c := range []byte(n.indices) {
                    if c == idxc {
                        n = n.children[i]
                        continue Walk
                    }
                }

                // 无匹配
                return
            }

            // 通配子节点处理
            n = n.children[0]

            switch n.nType {
            case param:
                // 提取参数直到 /
                end := 0
                for end < len(path) && path[end] != '/' {
                    end++
                }

                *params = append(*params, Param{
                    Key:   n.path[1:], // 去掉冒号
                    Value: path[:end],
                })

                if end < len(path) {
                    path = path[end:]
                    continue Walk
                }

                value.handlers = n.handlers
                return

            case catchAll:
                // 匹配剩余所有路径
                *params = append(*params, Param{
                    Key:   n.path[1:], // 去掉星号
                    Value: path,
                })
                value.handlers = n.handlers
                return
            }
        }
    }
}

查找性能分析

操作时间复杂度说明
静态路由O(k)k为路径长度
参数路由O(k)提取参数有少量开销
通配路由O(k)匹配剩余路径

注意:路由树构建时按优先级排序,高频路由会被优先匹配。

要点总结

  • Radix树通过公共前缀压缩节点,减少内存占用
  • 节点分割发生在公共前缀不匹配时
  • 参数节点(:param)和通配节点(*filepath)特殊处理
  • 查找时间复杂度O(k),k为路径长度
  • priority字段用于记录访问频率,优化热路径

📝 发现内容有误?点击此处直接编辑

← 上一篇 Gin中间件链执行机制
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库