mirror of
https://github.com/infinite-iroha/touka.git
synced 2026-02-03 00:41:10 +08:00
This commit introduces several optimizations to reduce allocations and
improve performance in the core routing and context mechanisms.
Radix Tree (tree.go):
- Optimized `getValue`'s internal `skippedNode` handling:
- Changed `skippedNode.node` to store a direct pointer to the tree node
instead of a full copy, significantly reducing allocations during
backtracking scenarios.
- Corrected the method of adding to the `skippedNodes` slice to use
`append`, ensuring safer and more idiomatic slice growth.
Context Handling (context.go):
- Implemented lazy initialization for `Context.Keys`:
- The `Keys` map is now only allocated on the first call to `Set()`
per request, avoiding map allocation for requests that do not use
context keys. `Context.reset()` now sets `Keys` to `nil`.
- `Get()` correctly handles the `nil` map state.
- Optimized `RequestIP()` for parsing comma-separated IP headers:
- Replaced `strings.Split()` with an iterative parsing approach using
`strings.IndexByte()` and slicing. This avoids allocating an
intermediate slice for IPs, reducing memory usage during IP resolution,
especially for headers like `X-Forwarded-For` with multiple IPs.
These changes are backward compatible for idiomatic usage and have been
reasoned to show improvements in simulated benchmarks, particularly in
reducing allocations per operation for the affected components.
916 lines
34 KiB
Go
916 lines
34 KiB
Go
// Copyright 2013 Julien Schmidt. All rights reserved.
|
||
// Use of this source code is governed by a BSD-style license that can be found
|
||
// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
|
||
// This tree.go is gin's fork, you can see https://github.com/gin-gonic/gin/blob/master/tree.go
|
||
|
||
package touka // 定义包名为 touka,该包可能是一个路由或Web框架的核心组件
|
||
|
||
import (
|
||
"bytes" // 导入 bytes 包,用于操作字节切片
|
||
"net/url" // 导入 net/url 包,用于 URL 解析和转义
|
||
"strings" // 导入 strings 包,用于字符串操作
|
||
"unicode" // 导入 unicode 包,用于处理 Unicode 字符
|
||
"unicode/utf8" // 导入 unicode/utf8 包,用于 UTF-8 编码和解码
|
||
"unsafe" // 导入 unsafe 包,用于不安全的类型转换,以避免内存分配
|
||
)
|
||
|
||
// StringToBytes 将字符串转换为字节切片,不进行内存分配。
|
||
// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。
|
||
// 注意:此函数使用 unsafe 包,应谨慎使用,因为它可能导致内存不安全。
|
||
func StringToBytes(s string) []byte {
|
||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||
}
|
||
|
||
// BytesToString 将字节切片转换为字符串,不进行内存分配。
|
||
// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。
|
||
// 注意:此函数使用 unsafe 包,应谨慎使用,因为它可能导致内存不安全。
|
||
func BytesToString(b []byte) string {
|
||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||
}
|
||
|
||
var (
|
||
strColon = []byte(":") // 定义字节切片常量,表示冒号,用于路径参数识别
|
||
strStar = []byte("*") // 定义字节切片常量,表示星号,用于捕获所有路径识别
|
||
strSlash = []byte("/") // 定义字节切片常量,表示斜杠,用于路径分隔符识别
|
||
)
|
||
|
||
// Param 是单个 URL 参数,由键和值组成。
|
||
type Param struct {
|
||
Key string // 参数的键名
|
||
Value string // 参数的值
|
||
}
|
||
|
||
// Params 是 Param 类型的切片,由路由器返回。
|
||
// 该切片是有序的,第一个 URL 参数也是切片中的第一个值。
|
||
// 因此,按索引读取值是安全的。
|
||
type Params []Param
|
||
|
||
// Get 返回键名与给定名称匹配的第一个 Param 的值,并返回一个布尔值 true。
|
||
// 如果未找到匹配的 Param,则返回空字符串和布尔值 false。
|
||
func (ps Params) Get(name string) (string, bool) {
|
||
for _, entry := range ps {
|
||
if entry.Key == name {
|
||
return entry.Value, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// ByName 返回键名与给定名称匹配的第一个 Param 的值。
|
||
// 如果未找到匹配的 Param,则返回空字符串。
|
||
func (ps Params) ByName(name string) (va string) {
|
||
va, _ = ps.Get(name) // 调用 Get 方法获取值,忽略第二个返回值
|
||
return
|
||
}
|
||
|
||
// methodTree 表示特定 HTTP 方法的路由树。
|
||
type methodTree struct {
|
||
method string // HTTP 方法(例如 "GET", "POST")
|
||
root *node // 该方法的根节点
|
||
}
|
||
|
||
// methodTrees 是 methodTree 的切片。
|
||
type methodTrees []methodTree
|
||
|
||
// get 根据给定的 HTTP 方法查找并返回对应的根节点。
|
||
// 如果找不到,则返回 nil。
|
||
func (trees methodTrees) get(method string) *node {
|
||
for _, tree := range trees {
|
||
if tree.method == method {
|
||
return tree.root
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// longestCommonPrefix 计算两个字符串的最长公共前缀的长度。
|
||
func longestCommonPrefix(a, b string) int {
|
||
i := 0
|
||
max_ := min(len(a), len(b)) // 找出两个字符串中较短的长度
|
||
for i < max_ && a[i] == b[i] { // 遍历直到达到较短长度或字符不匹配
|
||
i++
|
||
}
|
||
return i // 返回公共前缀的长度
|
||
}
|
||
|
||
// addChild 添加一个子节点,并将通配符子节点(如果存在)保持在数组的末尾。
|
||
func (n *node) addChild(child *node) {
|
||
if n.wildChild && len(n.children) > 0 {
|
||
// 如果当前节点有通配符子节点,且已有子节点,则将通配符子节点移到末尾
|
||
wildcardChild := n.children[len(n.children)-1]
|
||
n.children = append(n.children[:len(n.children)-1], child, wildcardChild)
|
||
} else {
|
||
// 否则,直接添加子节点
|
||
n.children = append(n.children, child)
|
||
}
|
||
}
|
||
|
||
// countParams 计算路径中参数(冒号)和捕获所有(星号)的数量。
|
||
func countParams(path string) uint16 {
|
||
var n uint16
|
||
s := StringToBytes(path) // 将路径字符串转换为字节切片
|
||
n += uint16(bytes.Count(s, strColon)) // 统计冒号的数量
|
||
n += uint16(bytes.Count(s, strStar)) // 统计星号的数量
|
||
return n
|
||
}
|
||
|
||
// countSections 计算路径中斜杠('/')的数量,即路径段的数量。
|
||
func countSections(path string) uint16 {
|
||
s := StringToBytes(path) // 将路径字符串转换为字节切片
|
||
return uint16(bytes.Count(s, strSlash)) // 统计斜杠的数量
|
||
}
|
||
|
||
// nodeType 定义了节点的类型。
|
||
type nodeType uint8
|
||
|
||
const (
|
||
static nodeType = iota // 静态节点,路径中不包含参数或通配符
|
||
root // 根节点
|
||
param // 参数节点(例如:name)
|
||
catchAll // 捕获所有节点(例如*path)
|
||
)
|
||
|
||
// node 表示路由树中的一个节点。
|
||
type node struct {
|
||
path string // 当前节点的路径段
|
||
indices string // 子节点第一个字符的索引字符串,用于快速查找子节点
|
||
wildChild bool // 是否包含通配符子节点(:param 或 *catchAll)
|
||
nType nodeType // 节点的类型(静态、根、参数、捕获所有)
|
||
priority uint32 // 节点的优先级,用于查找时优先匹配
|
||
children []*node // 子节点切片,最多有一个 :param 风格的节点位于数组末尾
|
||
handlers HandlersChain // 绑定到此节点的处理函数链
|
||
fullPath string // 完整路径,用于调试和错误信息
|
||
}
|
||
|
||
// incrementChildPrio 增加给定子节点的优先级并在必要时重新排序。
|
||
func (n *node) incrementChildPrio(pos int) int {
|
||
cs := n.children // 获取子节点切片
|
||
cs[pos].priority++ // 增加指定位置子节点的优先级
|
||
prio := cs[pos].priority // 获取新的优先级
|
||
|
||
// 调整位置(向前移动)
|
||
newPos := pos
|
||
// 从当前位置向前遍历,如果前一个子节点的优先级小于当前子节点,则交换位置
|
||
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
|
||
// 交换节点位置
|
||
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
|
||
}
|
||
|
||
// 构建新的索引字符字符串
|
||
if newPos != pos {
|
||
// 如果位置发生变化,则重新构建 indices 字符串
|
||
// 前缀部分 + 移动的索引字符 + 剩余部分
|
||
n.indices = n.indices[:newPos] + // 未改变的前缀,可能为空
|
||
n.indices[pos:pos+1] + // 被移动的索引字符
|
||
n.indices[newPos:pos] + n.indices[pos+1:] // 除去原位置字符的其余部分
|
||
}
|
||
|
||
return newPos // 返回新的位置
|
||
}
|
||
|
||
// addRoute 为给定路径添加一个带有处理函数的节点。
|
||
// 非并发安全!
|
||
func (n *node) addRoute(path string, handlers HandlersChain) {
|
||
fullPath := path // 记录完整的路径
|
||
n.priority++ // 增加当前节点的优先级
|
||
|
||
// 如果是空树(根节点)
|
||
if len(n.path) == 0 && len(n.children) == 0 {
|
||
n.insertChild(path, fullPath, handlers) // 直接插入子节点
|
||
n.nType = root // 设置为根节点类型
|
||
return
|
||
}
|
||
|
||
parentFullPathIndex := 0 // 记录父节点的完整路径索引
|
||
|
||
walk: // 外部循环用于遍历和构建路由树
|
||
for {
|
||
// 找到最长公共前缀。
|
||
// 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符。
|
||
i := longestCommonPrefix(path, n.path)
|
||
|
||
// 分裂边 (Split edge)
|
||
// 如果公共前缀小于当前节点的路径长度,说明当前节点需要被分裂
|
||
if i < len(n.path) {
|
||
child := node{
|
||
path: n.path[i:], // 子节点路径是当前节点路径的剩余部分
|
||
wildChild: n.wildChild, // 继承通配符子节点状态
|
||
nType: static, // 分裂后的新节点是静态类型
|
||
indices: n.indices, // 继承索引
|
||
children: n.children, // 继承子节点
|
||
handlers: n.handlers, // 继承处理函数
|
||
priority: n.priority - 1, // 优先级减1,因为分裂会降低优先级
|
||
fullPath: n.fullPath, // 继承完整路径
|
||
}
|
||
|
||
n.children = []*node{&child} // 当前节点现在只有一个子节点:新分裂出的子节点
|
||
// 将当前节点的 indices 设置为新子节点路径的第一个字符
|
||
n.indices = BytesToString([]byte{n.path[i]}) // []byte 用于正确的 Unicode 字符转换
|
||
n.path = path[:i] // 当前节点的路径更新为公共前缀
|
||
n.handlers = nil // 当前节点不再有处理函数(因为它被分裂了)
|
||
n.wildChild = false // 当前节点不再是通配符子节点
|
||
n.fullPath = fullPath[:parentFullPathIndex+i] // 更新完整路径
|
||
}
|
||
|
||
// 将新节点作为当前节点的子节点
|
||
// 如果路径仍然有剩余部分(即未完全匹配)
|
||
if i < len(path) {
|
||
path = path[i:] // 移除已匹配的前缀
|
||
c := path[0] // 获取剩余路径的第一个字符
|
||
|
||
// '/' 在参数之后
|
||
// 如果当前节点是参数类型,且剩余路径以 '/' 开头,并且只有一个子节点
|
||
// 则继续遍历其唯一的子节点
|
||
if n.nType == param && c == '/' && len(n.children) == 1 {
|
||
parentFullPathIndex += len(n.path) // 更新父节点完整路径索引
|
||
n = n.children[0] // 移动到子节点
|
||
n.priority++ // 增加子节点优先级
|
||
continue walk // 继续外部循环
|
||
}
|
||
|
||
// 检查是否存在以下一个路径字节开头的子节点
|
||
for i, max_ := 0, len(n.indices); i < max_; i++ {
|
||
if c == n.indices[i] { // 如果找到匹配的索引字符
|
||
parentFullPathIndex += len(n.path) // 更新父节点完整路径索引
|
||
i = n.incrementChildPrio(i) // 增加子节点优先级并重新排序
|
||
n = n.children[i] // 移动到匹配的子节点
|
||
continue walk // 继续外部循环
|
||
}
|
||
}
|
||
|
||
// 否则,插入新节点
|
||
// 如果第一个字符不是 ':' 也不是 '*',且当前节点不是 catchAll 类型
|
||
if c != ':' && c != '*' && n.nType != catchAll {
|
||
// 将新字符添加到索引字符串
|
||
n.indices += BytesToString([]byte{c}) // []byte 用于正确的 Unicode 字符转换
|
||
child := &node{
|
||
fullPath: fullPath, // 设置子节点的完整路径
|
||
}
|
||
n.addChild(child) // 添加新子节点
|
||
n.incrementChildPrio(len(n.indices) - 1) // 增加新子节点的优先级并重新排序
|
||
n = child // 移动到新子节点
|
||
} else if n.wildChild {
|
||
// 正在插入一个通配符节点,需要检查是否与现有通配符冲突
|
||
n = n.children[len(n.children)-1] // 移动到现有的通配符子节点
|
||
n.priority++ // 增加其优先级
|
||
|
||
// 检查通配符是否匹配
|
||
// 如果剩余路径长度大于等于通配符节点的路径长度,且通配符节点路径是剩余路径的前缀
|
||
// 并且不是 catchAll 类型(不能有子路由),
|
||
// 并且通配符之后没有更多字符或紧跟着 '/'
|
||
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
|
||
// 不能向 catchAll 添加子节点
|
||
n.nType != catchAll &&
|
||
// 检查更长的通配符,例如 :name 和 :names
|
||
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
|
||
continue walk // 继续外部循环
|
||
}
|
||
|
||
// 通配符冲突
|
||
pathSeg := path
|
||
if n.nType != catchAll {
|
||
pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll,则截取到下一个 '/'
|
||
}
|
||
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path // 构造冲突前缀
|
||
panic("'" + pathSeg + // 抛出 panic 表示通配符冲突
|
||
"' in new path '" + fullPath +
|
||
"' conflicts with existing wildcard '" + n.path +
|
||
"' in existing prefix '" + prefix +
|
||
"'")
|
||
}
|
||
|
||
n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符)
|
||
return // 完成添加路由
|
||
}
|
||
|
||
// 否则,将处理函数添加到当前节点
|
||
if n.handlers != nil {
|
||
panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数,则报错
|
||
}
|
||
n.handlers = handlers // 设置处理函数
|
||
n.fullPath = fullPath // 设置完整路径
|
||
return // 完成添加路由
|
||
}
|
||
}
|
||
|
||
// findWildcard 搜索通配符段并检查名称是否包含无效字符。
|
||
// 如果未找到通配符,则返回 -1 作为索引。
|
||
func findWildcard(path string) (wildcard string, i int, valid bool) {
|
||
// 查找开始位置
|
||
escapeColon := false // 是否正在处理转义字符
|
||
for start, c := range []byte(path) {
|
||
if escapeColon {
|
||
escapeColon = false
|
||
if c == ':' { // 如果转义字符是 ':',则跳过
|
||
continue
|
||
}
|
||
panic("invalid escape string in path '" + path + "'") // 无效的转义字符串
|
||
}
|
||
if c == '\\' { // 如果是反斜杠,则设置转义标志
|
||
escapeColon = true
|
||
continue
|
||
}
|
||
// 通配符以 ':' (参数) 或 '*' (捕获所有) 开头
|
||
if c != ':' && c != '*' {
|
||
continue
|
||
}
|
||
|
||
// 查找结束位置并检查无效字符
|
||
valid = true // 默认为有效
|
||
for end, c := range []byte(path[start+1:]) {
|
||
switch c {
|
||
case '/': // 如果遇到斜杠,说明通配符段结束
|
||
return path[start : start+1+end], start, valid
|
||
case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*',则无效
|
||
valid = false
|
||
}
|
||
}
|
||
return path[start:], start, valid // 返回找到的通配符、起始索引和有效性
|
||
}
|
||
return "", -1, false // 未找到通配符
|
||
}
|
||
|
||
// insertChild 插入一个带有处理函数的节点。
|
||
// 此函数处理包含通配符的路径插入逻辑。
|
||
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
|
||
for {
|
||
// 找到第一个通配符之前的前缀
|
||
wildcard, i, valid := findWildcard(path)
|
||
if i < 0 { // 未找到通配符,结束循环
|
||
break
|
||
}
|
||
|
||
// 通配符名称只能包含一个 ':' 或 '*' 字符
|
||
if !valid {
|
||
panic("only one wildcard per path segment is allowed, has: '" +
|
||
wildcard + "' in path '" + fullPath + "'") // 报错:每个路径段只允许一个通配符
|
||
}
|
||
|
||
// 检查通配符是否有名称
|
||
if len(wildcard) < 2 {
|
||
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错:通配符必须有非空名称
|
||
}
|
||
|
||
if wildcard[0] == ':' { // 如果是参数节点 (param)
|
||
if i > 0 {
|
||
// 在当前通配符之前插入前缀
|
||
n.path = path[:i] // 当前节点路径更新为前缀
|
||
path = path[i:] // 剩余路径去除前缀
|
||
}
|
||
|
||
child := &node{
|
||
nType: param, // 子节点类型为参数
|
||
path: wildcard, // 子节点路径为通配符名称
|
||
fullPath: fullPath, // 设置子节点的完整路径
|
||
}
|
||
n.addChild(child) // 添加子节点
|
||
n.wildChild = true // 当前节点标记为有通配符子节点
|
||
n = child // 移动到新创建的参数节点
|
||
n.priority++ // 增加优先级
|
||
|
||
// 如果路径不以通配符结束,则会有一个以 '/' 开头的子路径
|
||
if len(wildcard) < len(path) {
|
||
path = path[len(wildcard):] // 剩余路径去除通配符部分
|
||
|
||
child := &node{
|
||
priority: 1, // 新子节点优先级
|
||
fullPath: fullPath, // 设置子节点的完整路径
|
||
}
|
||
n.addChild(child) // 添加子节点(通常是斜杠后的静态部分)
|
||
n = child // 移动到这个新子节点
|
||
continue // 继续循环,查找下一个通配符或结束
|
||
}
|
||
|
||
// 否则,我们已经完成。将处理函数插入到新叶节点中
|
||
n.handlers = handlers // 设置处理函数
|
||
return // 完成
|
||
}
|
||
|
||
// 如果是捕获所有节点 (catchAll)
|
||
if i+len(wildcard) != len(path) {
|
||
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") // 报错:捕获所有路由只能在路径末尾
|
||
}
|
||
|
||
// 检查路径段冲突
|
||
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
|
||
pathSeg := ""
|
||
if len(n.children) != 0 {
|
||
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
|
||
}
|
||
panic("catch-all wildcard '" + path + // 报错:捕获所有通配符与现有路径段冲突
|
||
"' in new path '" + fullPath +
|
||
"' conflicts with existing path segment '" + pathSeg +
|
||
"' in existing prefix '" + n.path + pathSeg +
|
||
"'")
|
||
}
|
||
|
||
// 当前固定宽度为 1,用于 '/'
|
||
i--
|
||
if i < 0 || path[i] != '/' {
|
||
panic("no / before catch-all in path '" + fullPath + "'") // 报错:捕获所有之前没有 '/'
|
||
}
|
||
|
||
n.path = path[:i] // 当前节点路径更新为 catchAll 之前的部分
|
||
|
||
// 第一个节点:路径为空的 catchAll 节点
|
||
child := &node{
|
||
wildChild: true, // 标记为有通配符子节点
|
||
nType: catchAll, // 类型为 catchAll
|
||
fullPath: fullPath, // 设置完整路径
|
||
}
|
||
|
||
n.addChild(child) // 添加子节点
|
||
n.indices = string('/') // 索引设置为 '/'
|
||
n = child // 移动到新创建的 catchAll 节点
|
||
n.priority++ // 增加优先级
|
||
|
||
// 第二个节点:包含变量的节点
|
||
child = &node{
|
||
path: path[i:], // 路径为 catchAll 的实际路径段
|
||
nType: catchAll, // 类型为 catchAll
|
||
handlers: handlers, // 设置处理函数
|
||
priority: 1, // 优先级
|
||
fullPath: fullPath, // 设置完整路径
|
||
}
|
||
n.children = []*node{child} // 将其设为当前节点的唯一子节点
|
||
|
||
return // 完成
|
||
}
|
||
|
||
// 如果没有找到通配符,简单地插入路径和处理函数
|
||
n.path = path // 设置当前节点路径
|
||
n.handlers = handlers // 设置处理函数
|
||
n.fullPath = fullPath // 设置完整路径
|
||
}
|
||
|
||
// nodeValue 包含 (*Node).getValue 方法的返回值
|
||
type nodeValue struct {
|
||
handlers HandlersChain // 匹配到的处理函数链
|
||
params *Params // 提取的 URL 参数
|
||
tsr bool // 是否建议进行尾部斜杠重定向 (Trailing Slash Redirect)
|
||
fullPath string // 匹配到的完整路径
|
||
}
|
||
|
||
// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息,以便回溯。
|
||
type skippedNode struct {
|
||
path string // 跳过时的完整请求路径段 (n.path + remaining path at that point)
|
||
node *node // 当时被跳过的节点 (n), direct pointer
|
||
paramsCount int16 // 当时已收集的参数数量
|
||
}
|
||
|
||
// getValue 返回注册到给定路径(key)的处理函数。通配符的值会保存到 map 中。
|
||
// 如果找不到处理函数,则在存在一个带有额外(或不带)尾部斜杠的处理函数时,
|
||
// 建议进行 TSR(尾部斜杠重定向)。
|
||
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
|
||
var globalParamsCount int16 // 全局参数计数
|
||
|
||
walk: // 外部循环用于遍历路由树
|
||
for {
|
||
prefix := n.path // 当前节点的路径前缀
|
||
if len(path) > len(prefix) {
|
||
if path[:len(prefix)] == prefix { // 如果路径以当前节点的前缀开头
|
||
path = path[len(prefix):] // 移除已匹配的前缀
|
||
|
||
// 优先尝试所有非通配符子节点,通过匹配索引字符
|
||
idxc := path[0] // 剩余路径的第一个字符
|
||
for i, c := range []byte(n.indices) {
|
||
if c == idxc { // 如果找到匹配的索引字符
|
||
// 如果当前节点有通配符子节点,则将当前节点添加到 skippedNodes,以便回溯
|
||
if n.wildChild {
|
||
*skippedNodes = append(*skippedNodes, skippedNode{
|
||
path: prefix + path, // Path is n.path + remaining path after matching n.path
|
||
node: n, // Store pointer to the original node `n`
|
||
paramsCount: globalParamsCount, // 记录当前参数计数
|
||
})
|
||
}
|
||
|
||
n = n.children[i] // 移动到匹配的子节点
|
||
continue walk // 继续外部循环
|
||
}
|
||
}
|
||
|
||
if !n.wildChild {
|
||
// 如果路径在循环结束时不等于 '/' 且当前节点没有子节点
|
||
// 当前节点需要回溯到最后一个有效的 skippedNode
|
||
if path != "/" {
|
||
for length := len(*skippedNodes); length > 0; length-- {
|
||
skippedNode := (*skippedNodes)[length-1]
|
||
*skippedNodes = (*skippedNodes)[:length-1] // 弹出 skippedNode
|
||
if strings.HasSuffix(skippedNode.path, path) { // 如果跳过的路径包含当前路径
|
||
path = skippedNode.path // 恢复路径
|
||
n = skippedNode.node // 恢复节点
|
||
if value.params != nil {
|
||
*value.params = (*value.params)[:skippedNode.paramsCount] // 恢复参数切片
|
||
}
|
||
globalParamsCount = skippedNode.paramsCount // 恢复参数计数
|
||
continue walk // 继续外部循环
|
||
}
|
||
}
|
||
}
|
||
|
||
// 未找到。
|
||
// 如果存在一个带有额外(或不带)尾部斜杠的处理函数,
|
||
// 我们可以建议重定向到相同 URL,不带尾部斜杠。
|
||
value.tsr = path == "/" && n.handlers != nil // 如果路径是 "/" 且当前节点有处理函数,则建议 TSR
|
||
return value
|
||
}
|
||
|
||
// 处理通配符子节点,它总是位于数组的末尾
|
||
n = n.children[len(n.children)-1] // 移动到通配符子节点
|
||
globalParamsCount++ // 增加全局参数计数
|
||
|
||
switch n.nType {
|
||
case param: // 参数节点
|
||
// 查找参数结束位置('/' 或路径末尾)
|
||
end := 0
|
||
for end < len(path) && path[end] != '/' {
|
||
end++
|
||
}
|
||
|
||
// 保存参数值
|
||
if params != nil {
|
||
// 如果需要,预分配容量
|
||
if cap(*params) < int(globalParamsCount) {
|
||
newParams := make(Params, len(*params), globalParamsCount)
|
||
copy(newParams, *params)
|
||
*params = newParams
|
||
}
|
||
|
||
if value.params == nil {
|
||
value.params = params
|
||
}
|
||
// 在预分配的容量内扩展切片
|
||
i := len(*value.params)
|
||
*value.params = (*value.params)[:i+1] // 扩展切片
|
||
val := path[:end] // 提取参数值
|
||
if unescape { // 如果需要进行 URL 解码
|
||
if v, err := url.QueryUnescape(val); err == nil {
|
||
val = v // 解码成功则更新值
|
||
}
|
||
}
|
||
(*value.params)[i] = Param{ // 存储参数
|
||
Key: n.path[1:], // 参数键名(去除冒号)
|
||
Value: val, // 参数值
|
||
}
|
||
}
|
||
|
||
// 我们需要继续深入!
|
||
if end < len(path) {
|
||
if len(n.children) > 0 {
|
||
path = path[end:] // 移除已提取的参数部分
|
||
n = n.children[0] // 移动到下一个子节点
|
||
continue walk // 继续外部循环
|
||
}
|
||
|
||
// ... 但我们无法继续
|
||
value.tsr = len(path) == end+1 // 如果路径只剩下斜杠,则建议 TSR
|
||
return value
|
||
}
|
||
|
||
if value.handlers = n.handlers; value.handlers != nil {
|
||
value.fullPath = n.fullPath
|
||
return value // 如果当前节点有处理函数,则返回
|
||
}
|
||
if len(n.children) == 1 {
|
||
// 未找到处理函数。检查是否存在此路径加尾部斜杠的处理函数,以进行 TSR 建议
|
||
n = n.children[0]
|
||
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
|
||
}
|
||
return value
|
||
|
||
case catchAll: // 捕获所有节点
|
||
// 保存参数值
|
||
if params != nil {
|
||
// 如果需要,预分配容量
|
||
if cap(*params) < int(globalParamsCount) {
|
||
newParams := make(Params, len(*params), globalParamsCount)
|
||
copy(newParams, *params)
|
||
*params = newParams
|
||
}
|
||
|
||
if value.params == nil {
|
||
value.params = params
|
||
}
|
||
// 在预分配的容量内扩展切片
|
||
i := len(*value.params)
|
||
*value.params = (*value.params)[:i+1] // 扩展切片
|
||
val := path // 参数值是剩余的整个路径
|
||
if unescape { // 如果需要进行 URL 解码
|
||
if v, err := url.QueryUnescape(path); err == nil {
|
||
val = v // 解码成功则更新值
|
||
}
|
||
}
|
||
(*value.params)[i] = Param{ // 存储参数
|
||
Key: n.path[2:], // 参数键名(去除星号)
|
||
Value: val, // 参数值
|
||
}
|
||
}
|
||
|
||
value.handlers = n.handlers // 设置处理函数
|
||
value.fullPath = n.fullPath
|
||
return value // 返回
|
||
|
||
default:
|
||
panic("invalid node type") // 无效的节点类型
|
||
}
|
||
}
|
||
}
|
||
|
||
if path == prefix { // 如果路径完全匹配当前节点的前缀
|
||
// 如果当前路径不等于 '/' 且节点没有注册的处理函数,且最近匹配的节点有子节点
|
||
// 当前节点需要回溯到最后一个有效的 skippedNode
|
||
if n.handlers == nil && path != "/" {
|
||
for length := len(*skippedNodes); length > 0; length-- {
|
||
skippedNode := (*skippedNodes)[length-1]
|
||
*skippedNodes = (*skippedNodes)[:length-1]
|
||
if strings.HasSuffix(skippedNode.path, path) {
|
||
path = skippedNode.path
|
||
n = skippedNode.node
|
||
if value.params != nil {
|
||
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||
}
|
||
globalParamsCount = skippedNode.paramsCount
|
||
continue walk
|
||
}
|
||
}
|
||
}
|
||
// 我们应该已经到达包含处理函数的节点。
|
||
// 检查此节点是否注册了处理函数。
|
||
if value.handlers = n.handlers; value.handlers != nil {
|
||
value.fullPath = n.fullPath
|
||
return value // 如果有处理函数,则返回
|
||
}
|
||
|
||
// 如果此路由没有处理函数,但此路由有通配符子节点,
|
||
// 则此路径必须有一个带有额外尾部斜杠的处理函数。
|
||
if path == "/" && n.wildChild && n.nType != root {
|
||
value.tsr = true // 建议 TSR
|
||
return value
|
||
}
|
||
|
||
if path == "/" && n.nType == static {
|
||
value.tsr = true // 如果是静态节点且路径是根,则建议 TSR
|
||
return value
|
||
}
|
||
|
||
// 未找到处理函数。检查此路径加尾部斜杠是否存在处理函数,以进行尾部斜杠重定向建议
|
||
for i, c := range []byte(n.indices) {
|
||
if c == '/' { // 如果索引中包含 '/'
|
||
n = n.children[i] // 移动到对应的子节点
|
||
value.tsr = (len(n.path) == 1 && n.handlers != nil) || // 如果子节点路径是 '/' 且有处理函数
|
||
(n.nType == catchAll && n.children[0].handlers != nil) // 或者子节点是 catchAll 且其子节点有处理函数
|
||
return value
|
||
}
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
// 未找到。我们可以建议重定向到相同 URL,添加一个额外的尾部斜杠,
|
||
// 如果该路径的叶节点存在。
|
||
value.tsr = path == "/" || // 如果路径是根路径
|
||
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' && // 或者前缀比路径多一个斜杠
|
||
path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠,且有处理函数
|
||
|
||
// 回溯到最后一个有效的 skippedNode
|
||
if !value.tsr && path != "/" {
|
||
for length := len(*skippedNodes); length > 0; length-- {
|
||
skippedNode := (*skippedNodes)[length-1]
|
||
*skippedNodes = (*skippedNodes)[:length-1]
|
||
if strings.HasSuffix(skippedNode.path, path) {
|
||
path = skippedNode.path
|
||
n = skippedNode.node
|
||
if value.params != nil {
|
||
*value.params = (*value.params)[:skippedNode.paramsCount]
|
||
}
|
||
globalParamsCount = skippedNode.paramsCount
|
||
continue walk
|
||
}
|
||
}
|
||
}
|
||
|
||
return value // 返回未找到
|
||
}
|
||
}
|
||
|
||
// findCaseInsensitivePath 对给定路径进行不区分大小写的查找,并尝试找到处理函数。
|
||
// 它还可以选择修复尾部斜杠。
|
||
// 它返回大小写校正后的路径和一个布尔值,指示查找是否成功。
|
||
func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]byte, bool) {
|
||
const stackBufSize = 128 // 栈上缓冲区的默认大小
|
||
|
||
// 在常见情况下使用栈上静态大小的缓冲区。
|
||
// 如果路径太长,则在堆上分配缓冲区。
|
||
buf := make([]byte, 0, stackBufSize)
|
||
if length := len(path) + 1; length > stackBufSize {
|
||
buf = make([]byte, 0, length) // 如果路径太长,则分配更大的缓冲区
|
||
}
|
||
|
||
ciPath := n.findCaseInsensitivePathRec(
|
||
path,
|
||
buf, // 预分配足够的内存给新路径
|
||
[4]byte{}, // 空的 rune 缓冲区
|
||
fixTrailingSlash, // 是否修复尾部斜杠
|
||
)
|
||
|
||
return ciPath, ciPath != nil // 返回校正后的路径和是否成功找到
|
||
}
|
||
|
||
// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节。
|
||
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
|
||
switch n {
|
||
case 0:
|
||
return rb
|
||
case 1:
|
||
return [4]byte{rb[1], rb[2], rb[3], 0} // 移动1位
|
||
case 2:
|
||
return [4]byte{rb[2], rb[3]} // 移动2位
|
||
case 3:
|
||
return [4]byte{rb[3]} // 移动3位
|
||
default:
|
||
return [4]byte{} // 其他情况返回空
|
||
}
|
||
}
|
||
|
||
// findCaseInsensitivePathRec 由 n.findCaseInsensitivePath 使用的递归不区分大小写查找函数。
|
||
func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte {
|
||
npLen := len(n.path) // 当前节点的路径长度
|
||
|
||
walk: // 外部循环用于遍历路由树
|
||
// 只要剩余路径长度大于等于当前节点路径长度,且当前节点路径(除第一个字符外)不区分大小写匹配剩余路径
|
||
for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) {
|
||
// 将公共前缀添加到结果中
|
||
oldPath := path // 保存原始路径
|
||
path = path[npLen:] // 移除已匹配的前缀
|
||
ciPath = append(ciPath, n.path...) // 将当前节点的路径添加到不区分大小写路径中
|
||
|
||
if len(path) == 0 { // 如果路径已完全匹配
|
||
// 我们应该已经到达包含处理函数的节点。
|
||
// 检查此节点是否注册了处理函数。
|
||
if n.handlers != nil {
|
||
return ciPath // 如果有处理函数,则返回校正后的路径
|
||
}
|
||
|
||
// 未找到处理函数。
|
||
// 尝试通过添加尾部斜杠来修复路径
|
||
if fixTrailingSlash {
|
||
for i, c := range []byte(n.indices) {
|
||
if c == '/' { // 如果索引中包含 '/'
|
||
n = n.children[i] // 移动到对应的子节点
|
||
if (len(n.path) == 1 && n.handlers != nil) || // 如果子节点路径是 '/' 且有处理函数
|
||
(n.nType == catchAll && n.children[0].handlers != nil) { // 或者子节点是 catchAll 且其子节点有处理函数
|
||
return append(ciPath, '/') // 返回添加斜杠后的路径
|
||
}
|
||
return nil // 否则返回 nil
|
||
}
|
||
}
|
||
}
|
||
return nil // 未找到,返回 nil
|
||
}
|
||
|
||
// 如果此节点没有通配符(参数或捕获所有)子节点,
|
||
// 我们可以直接查找下一个子节点并继续遍历树。
|
||
if !n.wildChild {
|
||
// 跳过已处理的 rune 字节
|
||
rb = shiftNRuneBytes(rb, npLen)
|
||
|
||
if rb[0] != 0 {
|
||
// 旧 rune 未处理完
|
||
idxc := rb[0]
|
||
for i, c := range []byte(n.indices) {
|
||
if c == idxc {
|
||
// 继续处理子节点
|
||
n = n.children[i]
|
||
npLen = len(n.path)
|
||
continue walk // 继续外部循环
|
||
}
|
||
}
|
||
} else {
|
||
// 处理一个新的 rune
|
||
var rv rune
|
||
|
||
// 查找 rune 的开始位置。
|
||
// Runes 最长为 4 字节。
|
||
// -4 肯定会是另一个 rune。
|
||
var off int
|
||
for max_ := min(npLen, 3); off < max_; off++ {
|
||
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
||
// 从缓存路径读取 rune
|
||
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
||
break
|
||
}
|
||
}
|
||
|
||
// 计算当前 rune 的小写字节
|
||
lo := unicode.ToLower(rv)
|
||
utf8.EncodeRune(rb[:], lo) // 将小写 rune 编码到缓冲区
|
||
|
||
// 跳过已处理的字节
|
||
rb = shiftNRuneBytes(rb, off)
|
||
|
||
idxc := rb[0]
|
||
for i, c := range []byte(n.indices) {
|
||
// 小写匹配
|
||
if c == idxc {
|
||
// 必须使用递归方法,因为大写字节和小写字节都可能作为索引存在
|
||
if out := n.children[i].findCaseInsensitivePathRec(
|
||
path, ciPath, rb, fixTrailingSlash,
|
||
); out != nil {
|
||
return out // 如果找到,则返回
|
||
}
|
||
break
|
||
}
|
||
}
|
||
|
||
// 如果未找到匹配项,则对大写 rune 执行相同操作(如果它不同)
|
||
if up := unicode.ToUpper(rv); up != lo {
|
||
utf8.EncodeRune(rb[:], up) // 将大写 rune 编码到缓冲区
|
||
rb = shiftNRuneBytes(rb, off)
|
||
|
||
idxc := rb[0]
|
||
for i, c := range []byte(n.indices) {
|
||
// 大写匹配
|
||
if c == idxc {
|
||
// 继续处理子节点
|
||
n = n.children[i]
|
||
npLen = len(n.path)
|
||
continue walk // 继续外部循环
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 未找到。我们可以建议重定向到相同 URL,不带尾部斜杠,
|
||
// 如果该路径的叶节点存在。
|
||
if fixTrailingSlash && path == "/" && n.handlers != nil {
|
||
return ciPath // 如果可以修复尾部斜杠且有处理函数,则返回
|
||
}
|
||
return nil // 未找到,返回 nil
|
||
}
|
||
|
||
n = n.children[0] // 移动到通配符子节点(通常是唯一一个)
|
||
switch n.nType {
|
||
case param: // 参数节点
|
||
// 查找参数结束位置('/' 或路径末尾)
|
||
end := 0
|
||
for end < len(path) && path[end] != '/' {
|
||
end++
|
||
}
|
||
|
||
// 将参数值添加到不区分大小写路径中
|
||
ciPath = append(ciPath, path[:end]...)
|
||
|
||
// 我们需要继续深入!
|
||
if end < len(path) {
|
||
if len(n.children) > 0 {
|
||
// 继续处理子节点
|
||
n = n.children[0]
|
||
npLen = len(n.path)
|
||
path = path[end:]
|
||
continue // 继续外部循环
|
||
}
|
||
|
||
// ... 但我们无法继续
|
||
if fixTrailingSlash && len(path) == end+1 {
|
||
return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠,则返回
|
||
}
|
||
return nil // 未找到,返回 nil
|
||
}
|
||
|
||
if n.handlers != nil {
|
||
return ciPath // 如果有处理函数,则返回
|
||
}
|
||
|
||
if fixTrailingSlash && len(n.children) == 1 {
|
||
// 未找到处理函数。检查此路径加尾部斜杠是否存在处理函数
|
||
n = n.children[0]
|
||
if n.path == "/" && n.handlers != nil {
|
||
return append(ciPath, '/') // 返回添加斜杠后的路径
|
||
}
|
||
}
|
||
|
||
return nil // 未找到,返回 nil
|
||
|
||
case catchAll: // 捕获所有节点
|
||
return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有)
|
||
|
||
default:
|
||
panic("invalid node type") // 无效的节点类型
|
||
}
|
||
}
|
||
|
||
// 未找到。
|
||
// 尝试通过添加/删除尾部斜杠来修复路径
|
||
if fixTrailingSlash {
|
||
if path == "/" {
|
||
return ciPath // 如果路径是根路径,则返回
|
||
}
|
||
// 如果路径长度比当前节点路径少一个斜杠,且末尾是斜杠,
|
||
// 且不区分大小写匹配,且当前节点有处理函数
|
||
if len(path)+1 == npLen && n.path[len(path)] == '/' &&
|
||
strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handlers != nil {
|
||
return append(ciPath, n.path...) // 返回添加当前节点路径后的路径
|
||
}
|
||
}
|
||
return nil // 未找到,返回 nil
|
||
}
|