Compare commits

...

30 commits
v0.3.5 ... main

Author SHA1 Message Date
WJQSERVER
a6e278d458 print errlog (jsonv2 marshal) 2026-01-26 08:08:01 +08:00
WJQSERVER
7b536ac137
Merge pull request #59 from infinite-iroha/fix-slice-panic
refactor: Improve engine's tree processing and context handling.
2025-12-15 00:05:02 +08:00
WJQSERVER
b348d7d41f update TempSkippedNodesPool 2025-12-14 23:42:50 +08:00
WJQSERVER
60b2936eff add TempSkippedNodesPool 2025-12-14 23:16:29 +08:00
WJQSERVER
9cfc82a347 chore: update go module dependencies. 2025-12-14 22:57:48 +08:00
WJQSERVER
904aea5df8 refactor: Improve engine's tree processing and context handling. 2025-12-14 22:56:37 +08:00
WJQSERVER
ee0ebc986c
Merge pull request #54 from infinite-iroha/dev
context added FileText method
2025-10-21 15:06:39 +08:00
wjqserver
e4aaaa1583 fix path to filepath 2025-10-21 15:06:26 +08:00
wjqserver
1361f6e237 update 2025-10-21 14:47:29 +08:00
WJQSERVER
a6458cca16
Merge pull request #53 from infinite-iroha/dev
update
2025-10-12 15:48:48 +08:00
wjqserver
76a89800a2 update 2025-10-12 15:47:02 +08:00
WJQSERVER
4955fb9d03
Merge pull request #52 from infinite-iroha/dev
fix StaticFS
2025-09-14 08:27:29 +08:00
wjqserver
5b98310de5 fix StaticFS 2025-09-14 08:24:01 +08:00
WJQSERVER
f1ac0dd6ff
Merge pull request #51 from infinite-iroha/dev
0.3.7
2025-09-10 02:40:51 +08:00
wjqserver
38ff5126e3 fix 2025-09-10 02:40:41 +08:00
WJQSERVER
b4e073ae2f
Update sse.go 2025-09-07 02:24:28 +08:00
WJQSERVER
af0a99acda add sse intn support 2025-09-06 17:55:45 +00:00
wjqserver
3ffde5742c add wanf 2025-08-20 16:50:26 +08:00
WJQSERVER
016df0efe4
Merge pull request #50 from infinite-iroha/dev
0.3.6
2025-08-01 10:27:01 +08:00
wjqserver
3590a77f90 fix reqip val 2025-08-01 10:23:49 +08:00
wjqserver
74f5770b42 update tree 2025-08-01 10:21:32 +08:00
WJQSERVER
0f4d90faeb
Merge pull request #49 from infinite-iroha/fix-router-panic
Fix router panic
2025-08-01 09:09:59 +08:00
wjqserver
783370fd79 update 2025-08-01 09:09:46 +08:00
wjqserver
295852e1a1 update reqip 2025-08-01 09:05:09 +08:00
wjqserver
99b48371b3 update test 2025-08-01 09:05:00 +08:00
google-labs-jules[bot]
e43b12e343 fix: correct shallow copy in router backtracking
The router could panic with a 'slice bounds out of range' error when handling requests that trigger its backtracking logic.

The root cause was a shallow copy of the node's `children` slice when creating a `skippedNode` for backtracking. This could lead to a corrupted state if the router needed to backtrack and then proceed down a wildcard path.

This commit fixes the issue by introducing a `copyChildren` method on the `node` struct, which creates a safe copy of the children slice. This method is now used when creating a `skippedNode`, ensuring that the backtracking logic is isolated and robust.
2025-08-01 00:49:53 +00:00
WJQSERVER
1e7682ad84
Merge pull request #48 from infinite-iroha/dev
add RunShutdownWithContext
2025-07-31 20:18:47 +08:00
WJQSERVER
2c60e84067
Merge pull request #46 from infinite-iroha/dev
0.3.4
2025-07-28 21:02:20 +08:00
WJQSERVER
dee05b048e
Merge pull request #45 from infinite-iroha/dev
update about
2025-07-27 16:07:33 +08:00
WJQSERVER
ccf25dee46
Merge pull request #44 from infinite-iroha/dev
fix cfdt
2025-07-25 00:35:43 +08:00
8 changed files with 697 additions and 247 deletions

View file

@ -14,16 +14,16 @@ import (
"io"
"math"
"mime"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"sync"
"time"
"github.com/WJQSERVER/wanf"
"github.com/fenthope/reco"
"github.com/go-json-experiment/json"
@ -43,7 +43,7 @@ type Context struct {
index int8 // 当前执行到处理链的哪个位置
mu sync.RWMutex
Keys map[string]interface{} // 用于在中间件之间传递数据
Keys map[string]any // 用于在中间件之间传递数据
Errors []error // 用于收集处理过程中的错误
@ -65,6 +65,10 @@ type Context struct {
// 请求体Body大小限制
MaxRequestBodySize int64
// skippedNodes 用于记录跳过的节点信息,以便回溯
// 通常在处理嵌套路由时使用
SkippedNodes []skippedNode
}
// --- Context 相关方法实现 ---
@ -78,20 +82,30 @@ func (c *Context) reset(w http.ResponseWriter, req *http.Request) {
} else {
c.Writer = newResponseWriter(w)
}
//c.Writer = newResponseWriter(w)
c.Request = req
c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组
//c.Params = c.Params[:0] // 清空 Params 切片,而不是重新分配,以复用底层数组
//避免params长度为0
if cap(c.Params) > 0 {
c.Params = c.Params[:0]
} else {
c.Params = make(Params, 0, c.engine.maxParams)
}
c.handlers = nil
c.index = -1 // 初始为 -1`Next()` 将其设置为 0
c.Keys = make(map[string]interface{}) // 每次请求重新创建 map避免数据污染
c.Keys = make(map[string]any) // 每次请求重新创建 map避免数据污染
c.Errors = c.Errors[:0] // 清空 Errors 切片
c.queryCache = nil // 清空查询参数缓存
c.formCache = nil // 清空表单数据缓存
c.ctx = req.Context() // 使用请求的上下文,继承其取消信号和值
c.sameSite = http.SameSiteDefaultMode // 默认 SameSite 模式
c.MaxRequestBodySize = c.engine.GlobalMaxRequestBodySize
// c.HTTPClient 和 c.engine 保持不变,它们引用 Engine 实例的成员
if cap(c.SkippedNodes) > 0 {
c.SkippedNodes = c.SkippedNodes[:0]
} else {
c.SkippedNodes = make([]skippedNode, 0, 256)
}
}
// Next 在处理链中执行下一个处理函数
@ -123,10 +137,10 @@ func (c *Context) AbortWithStatus(code int) {
// Set 将一个键值对存储到 Context 中
// 这是一个线程安全的操作,用于在中间件之间传递数据
func (c *Context) Set(key string, value interface{}) {
func (c *Context) Set(key string, value any) {
c.mu.Lock() // 加写锁
if c.Keys == nil {
c.Keys = make(map[string]interface{})
c.Keys = make(map[string]any)
}
c.Keys[key] = value
c.mu.Unlock() // 解写锁
@ -134,7 +148,7 @@ func (c *Context) Set(key string, value interface{}) {
// Get 从 Context 中获取一个值
// 这是一个线程安全的操作
func (c *Context) Get(key string) (value interface{}, exists bool) {
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock() // 加读锁
value, exists = c.Keys[key]
c.mu.RUnlock() // 解读锁
@ -209,7 +223,7 @@ func (c *Context) GetDuration(key string) (value time.Duration, exists bool) {
// MustGet 从 Context 中获取一个值,如果不存在则 panic
// 适用于确定值一定存在的场景
func (c *Context) MustGet(key string) interface{} {
func (c *Context) MustGet(key string) any {
if value, exists := c.Get(key); exists {
return value
}
@ -270,7 +284,7 @@ func (c *Context) Raw(code int, contentType string, data []byte) {
}
// String 向响应写入格式化的字符串
func (c *Context) String(code int, format string, values ...interface{}) {
func (c *Context) String(code int, format string, values ...any) {
c.Writer.WriteHeader(code)
c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}
@ -282,13 +296,121 @@ func (c *Context) Text(code int, text string) {
c.Writer.Write([]byte(text))
}
// FileText
func (c *Context) FileText(code int, filePath string) {
// 清理path
cleanPath := filepath.Clean(filePath)
if !filepath.IsAbs(cleanPath) {
c.AddError(fmt.Errorf("relative path not allowed: %s", cleanPath))
c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("relative path not allowed"))
return
}
// 检查文件是否存在
if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
c.AddError(fmt.Errorf("file not found: %s", cleanPath))
c.ErrorUseHandle(http.StatusNotFound, fmt.Errorf("file not found"))
return
}
// 打开文件
file, err := os.Open(cleanPath)
if err != nil {
c.AddError(fmt.Errorf("failed to open file %s: %w", cleanPath, err))
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to open file: %w", err))
return
}
defer file.Close()
// 获取文件信息以获取文件大小
fileInfo, err := file.Stat()
if err != nil {
c.AddError(fmt.Errorf("failed to get file info for %s: %w", cleanPath, err))
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to get file info: %w", err))
return
}
// 判断是否是dir
if fileInfo.IsDir() {
c.AddError(fmt.Errorf("path is a directory, not a file: %s", cleanPath))
c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("path is a directory"))
return
}
c.SetHeader("Content-Type", "text/plain; charset=utf-8")
c.SetBodyStream(file, int(fileInfo.Size()))
}
/*
// not fot work
// FileTextSafeDir
func (c *Context) FileTextSafeDir(code int, filePath string, safeDir string) {
// 清理path
cleanPath := path.Clean(filePath)
if !filepath.IsAbs(cleanPath) {
c.AddError(fmt.Errorf("relative path not allowed: %s", cleanPath))
c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("relative path not allowed"))
return
}
if strings.Contains(cleanPath, "..") {
c.AddError(fmt.Errorf("path traversal attempt detected: %s", cleanPath))
c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("path traversal attempt detected"))
return
}
// 判断filePath是否包含在safeDir内, 防止路径穿越
relPath, err := filepath.Rel(safeDir, cleanPath)
if err != nil {
c.AddError(fmt.Errorf("failed to get relative path: %w", err))
c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("failed to get relative path: %w", err))
return
}
cleanPath = filepath.Join(safeDir, relPath)
// 检查文件是否存在
if _, err := os.Stat(cleanPath); os.IsNotExist(err) {
c.AddError(fmt.Errorf("file not found: %s", cleanPath))
c.ErrorUseHandle(http.StatusNotFound, fmt.Errorf("file not found"))
return
}
// 打开文件
file, err := os.Open(cleanPath)
if err != nil {
c.AddError(fmt.Errorf("failed to open file %s: %w", cleanPath, err))
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to open file: %w", err))
return
}
defer file.Close()
// 获取文件信息以获取文件大小
fileInfo, err := file.Stat()
if err != nil {
c.AddError(fmt.Errorf("failed to get file info for %s: %w", cleanPath, err))
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to get file info: %w", err))
return
}
// 判断是否是dir
if fileInfo.IsDir() {
c.AddError(fmt.Errorf("path is a directory, not a file: %s", cleanPath))
c.ErrorUseHandle(http.StatusBadRequest, fmt.Errorf("path is a directory"))
return
}
c.SetHeader("Content-Type", "text/plain; charset=utf-8")
c.SetBodyStream(file, int(fileInfo.Size()))
}
*/
// JSON 向响应写入 JSON 数据
// 设置 Content-Type 为 application/json
func (c *Context) JSON(code int, obj interface{}) {
func (c *Context) JSON(code int, obj any) {
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.Writer.WriteHeader(code)
if err := json.MarshalWrite(c.Writer, obj); err != nil {
c.AddError(fmt.Errorf("failed to marshal JSON: %w", err))
c.Errorf("failed to marshal JSON: %s", err)
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to marshal JSON: %w", err))
return
}
@ -296,7 +418,7 @@ func (c *Context) JSON(code int, obj interface{}) {
// GOB 向响应写入GOB数据
// 设置 Content-Type 为 application/octet-stream
func (c *Context) GOB(code int, obj interface{}) {
func (c *Context) GOB(code int, obj any) {
c.Writer.Header().Set("Content-Type", "application/octet-stream") // 设置合适的 Content-Type
c.Writer.WriteHeader(code)
// GOB 编码
@ -308,11 +430,25 @@ func (c *Context) GOB(code int, obj interface{}) {
}
}
// WANF向响应写入WANF数据
// 设置 application/vnd.wjqserver.wanf; charset=utf-8
func (c *Context) WANF(code int, obj any) {
c.Writer.Header().Set("Content-Type", "application/vnd.wjqserver.wanf; charset=utf-8")
c.Writer.WriteHeader(code)
// WANF 编码
encoder := wanf.NewStreamEncoder(c.Writer)
if err := encoder.Encode(obj); err != nil {
c.AddError(fmt.Errorf("failed to encode WANF: %w", err))
c.ErrorUseHandle(http.StatusInternalServerError, fmt.Errorf("failed to encode WANF: %w", err))
return
}
}
// HTML 渲染 HTML 模板
// 如果 Engine 配置了 HTMLRender则使用它进行渲染
// 否则,会进行简单的字符串输出
// 预留接口,可以扩展为支持多种模板引擎
func (c *Context) HTML(code int, name string, obj interface{}) {
func (c *Context) HTML(code int, name string, obj any) {
c.Writer.Header().Set("Content-Type", "text/html; charset=utf-8")
c.Writer.WriteHeader(code)
@ -343,7 +479,7 @@ func (c *Context) Redirect(code int, location string) {
}
// ShouldBindJSON 尝试将请求体绑定到 JSON 对象
func (c *Context) ShouldBindJSON(obj interface{}) error {
func (c *Context) ShouldBindJSON(obj any) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
}
@ -354,10 +490,28 @@ func (c *Context) ShouldBindJSON(obj interface{}) error {
return nil
}
// ShouldBindWANF
func (c *Context) ShouldBindWANF(obj any) error {
if c.Request.Body == nil {
return errors.New("request body is empty")
}
decoder, err := wanf.NewStreamDecoder(c.Request.Body)
if err != nil {
return fmt.Errorf("failed to create WANF decoder: %w", err)
}
if err := decoder.Decode(obj); err != nil {
return fmt.Errorf("WANF binding error: %w", err)
}
return nil
}
// Deprecated: This function is a reserved placeholder for future API extensions
// and is not yet implemented. It will either be properly defined or removed in v2.0.0. Do not use.
// ShouldBind 尝试将请求体绑定到各种类型JSON, Form, XML 等)
// 这是一个复杂的通用绑定接口,通常根据 Content-Type 或其他头部来判断绑定方式
// 预留接口,可根据项目需求进行扩展
func (c *Context) ShouldBind(obj interface{}) error {
func (c *Context) ShouldBind(obj any) error {
// TODO: 完整的通用绑定逻辑
// 可以根据 c.Request.Header.Get("Content-Type") 来判断是 JSON, Form, XML 等
// 例如:
@ -410,7 +564,7 @@ func (c *Context) Err() error {
// Value returns the value associated with this context for key, or nil if no
// value is associated with key.
// 可以用于从 Context 中获取与特定键关联的值,包括 Go 原生 Context 的值和 Touka Context 的 Keys
func (c *Context) Value(key interface{}) interface{} {
func (c *Context) Value(key any) any {
if keyAsString, ok := key.(string); ok {
if val, exists := c.Get(keyAsString); exists {
return val
@ -523,39 +677,59 @@ func (c *Context) GetReqBodyBuffer() (*bytes.Buffer, error) {
func (c *Context) RequestIP() string {
if c.engine.ForwardByClientIP {
for _, headerName := range c.engine.RemoteIPHeaders {
if ipValue := c.Request.Header.Get(headerName); ipValue != "" {
// X-Forwarded-For 可能包含多个 IP约定第一个最左边是客户端 IP
// 其他头部(如 X-Real-IP通常只有一个
ips := strings.Split(ipValue, ",")
for _, singleIP := range ips {
trimmedIP := strings.TrimSpace(singleIP)
// 使用 netip.ParseAddr 进行 IP 地址的解析和格式验证
addr, err := netip.ParseAddr(trimmedIP)
if err == nil {
// 成功解析到合法的 IP 地址格式,立即返回
return addr.String()
}
// 如果当前 singleIP 无效,继续检查列表中的下一个
ipValue := c.Request.Header.Get(headerName)
if ipValue == "" {
continue // 头部为空, 继续检查下一个
}
// 使用索引高效遍历逗号分隔的 IP 列表, 避免 strings.Split 的内存分配
currentPos := 0
for currentPos < len(ipValue) {
nextComma := strings.IndexByte(ipValue[currentPos:], ',')
var ipSegment string
if nextComma == -1 {
// 这是列表中的最后一个 IP
ipSegment = ipValue[currentPos:]
currentPos = len(ipValue) // 结束循环
} else {
// 截取当前 IP 段
ipSegment = ipValue[currentPos : currentPos+nextComma]
currentPos += nextComma + 1 // 移动到下一个 IP 段的起始位置
}
// 去除空格并检查是否为空 (例如 "ip1,,ip2")
trimmedIP := strings.TrimSpace(ipSegment)
if trimmedIP == "" {
continue
}
// 使用 netip.ParseAddr 进行 IP 地址的解析和验证
addr, err := netip.ParseAddr(trimmedIP)
if err == nil {
// 成功解析到合法的 IP, 立即返回
return addr.String()
}
}
}
}
// 如果没有启用 ForwardByClientIP 或头部中没有找到有效 IP回退到 Request.RemoteAddr
// RemoteAddr 通常是 "host:port" 格式,但也可能直接就是 IP 地址
remoteAddrStr := c.Request.RemoteAddr
ip, _, err := net.SplitHostPort(remoteAddrStr) // 尝试分离 host 和 port
if err != nil {
// 如果分离失败,意味着 remoteAddrStr 可能直接就是 IP 地址(或畸形)
ip = remoteAddrStr // 此时将整个 remoteAddrStr 作为候选 IP
// 回退到 Request.RemoteAddr 的处理
// 优先使用 netip.ParseAddrPort, 它比 net.SplitHostPort 更高效且分配更少
addrp, err := netip.ParseAddrPort(c.Request.RemoteAddr)
if err == nil {
// 成功从 "ip:port" 格式中解析出 IP
return addrp.Addr().String()
}
// 对从 RemoteAddr 中提取/使用的 IP 进行最终的合法性验证
addr, parseErr := netip.ParseAddr(ip)
if parseErr == nil {
return addr.String() // 成功解析并返回合法 IP
// 如果上面的解析失败 (例如 RemoteAddr 只有 IP, 没有端口),
// 则尝试将整个字符串作为 IP 地址进行解析
addr, err := netip.ParseAddr(c.Request.RemoteAddr)
if err == nil {
return addr.String()
}
// 所有方法都失败, 返回空字符串
return ""
}
@ -705,7 +879,7 @@ func (c *Context) GetRequestURIPath() string {
// 将文件内容作为响应body
func (c *Context) SetRespBodyFile(code int, filePath string) {
// 清理path
cleanPath := path.Clean(filePath)
cleanPath := filepath.Clean(filePath)
// 打开文件
file, err := os.Open(cleanPath)
@ -725,7 +899,7 @@ func (c *Context) SetRespBodyFile(code int, filePath string) {
}
// 尝试根据文件扩展名猜测 Content-Type
contentType := mime.TypeByExtension(path.Ext(cleanPath))
contentType := mime.TypeByExtension(filepath.Ext(cleanPath))
if contentType == "" {
// 如果无法猜测,则使用默认的二进制流类型
contentType = "application/octet-stream"

View file

@ -421,6 +421,41 @@ func getHandlerName(h HandlerFunc) string {
}
const MaxSkippedNodesCap = 256
// TempSkippedNodesPool 存储 *[]skippedNode 以复用内存
var TempSkippedNodesPool = sync.Pool{
New: func() any {
// 返回一个指向容量为 256 的新切片的指针
s := make([]skippedNode, 0, MaxSkippedNodesCap)
return &s
},
}
// GetTempSkippedNodes 从 Pool 中获取一个 *[]skippedNode 指针
func GetTempSkippedNodes() *[]skippedNode {
// 直接返回 Pool 中存储的指针
return TempSkippedNodesPool.Get().(*[]skippedNode)
}
// PutTempSkippedNodes 将用完的 *[]skippedNode 指针放回 Pool
func PutTempSkippedNodes(skippedNodes *[]skippedNode) {
if skippedNodes == nil || *skippedNodes == nil {
return
}
// 检查容量是否符合预期。如果容量不足,则丢弃,不放回 Pool。
if cap(*skippedNodes) < MaxSkippedNodesCap {
return // 丢弃该对象,让 Pool 在下次 Get 时通过 New 重新分配
}
// 长度重置为 0保留容量实现复用
*skippedNodes = (*skippedNodes)[:0]
// 将指针存回 Pool
TempSkippedNodesPool.Put(skippedNodes)
}
// 405中间件
func MethodNotAllowed() HandlerFunc {
return func(c *Context) {
@ -432,9 +467,10 @@ func MethodNotAllowed() HandlerFunc {
// 如果是 OPTIONS 请求,尝试查找所有允许的方法
allowedMethods := []string{}
for _, treeIter := range engine.methodTrees {
var tempSkippedNodes []skippedNode
// 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型
value := treeIter.root.getValue(requestPath, nil, &tempSkippedNodes, false)
tempSkippedNodes := GetTempSkippedNodes()
value := treeIter.root.getValue(requestPath, nil, tempSkippedNodes, false)
PutTempSkippedNodes(tempSkippedNodes)
if value.handlers != nil {
allowedMethods = append(allowedMethods, treeIter.method)
}
@ -451,9 +487,10 @@ func MethodNotAllowed() HandlerFunc {
if treeIter.method == httpMethod { // 已经处理过当前方法,跳过
continue
}
var tempSkippedNodes []skippedNode // 用于临时查找,不影响主 Context
// 注意这里 treeIter.root 才是正确的,因为 treeIter 是 methodTree 类型
value := treeIter.root.getValue(requestPath, nil, &tempSkippedNodes, false) // 只查找是否存在,不需要参数
tempSkippedNodes := GetTempSkippedNodes()
value := treeIter.root.getValue(requestPath, nil, tempSkippedNodes, false) // 只查找是否存在,不需要参数
PutTempSkippedNodes(tempSkippedNodes)
if value.handlers != nil {
// 使用定义的ErrorHandle处理
engine.errorHandle.handler(c, http.StatusMethodNotAllowed, errors.New("method not allowed"))
@ -661,9 +698,8 @@ func (engine *Engine) handleRequest(c *Context) {
// 查找匹配的节点和处理函数
// 这里传递 &c.Params 而不是重新创建,以利用 Context 中预分配的容量
// skippedNodes 内部使用,因此无需从外部传入已分配的 slice
var skippedNodes []skippedNode // 用于回溯的跳过节点
// 直接在 rootNode 上调用 getValue 方法
value := rootNode.getValue(requestPath, &c.Params, &skippedNodes, true) // unescape=true 对路径参数进行 URL 解码
value := rootNode.getValue(requestPath, &c.Params, &c.SkippedNodes, true) // unescape=true 对路径参数进行 URL 解码
if value.handlers != nil {
//c.handlers = engine.combineHandlers(engine.globalHandlers, value.handlers) // 组合全局中间件和路由处理函数

View file

@ -6,7 +6,6 @@ package touka
import (
"errors"
"fmt"
"net/http"
"path"
"strings"
@ -19,13 +18,19 @@ var allowedFileServerMethods = map[string]struct{}{
http.MethodHead: {},
}
var (
ErrInputFSisNil = errors.New("input FS is nil")
ErrMethodNotAllowed = errors.New("method not allowed")
)
// FileServer方式, 返回一个HandleFunc, 统一化处理
func FileServer(fs http.FileSystem) HandlerFunc {
if fs == nil {
return func(c *Context) {
c.ErrorUseHandle(500, errors.New("Input FileSystem is nil"))
c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil)
}
}
fileServerInstance := http.FileServer(fs)
return func(c *Context) {
FileServerHandleServe(c, fileServerInstance)
@ -37,7 +42,6 @@ func FileServer(fs http.FileSystem) HandlerFunc {
func FileServerHandleServe(c *Context, fsHandle http.Handler) {
if fsHandle == nil {
ErrInputFSisNil := errors.New("Input FileSystem Handle is nil")
c.AddError(ErrInputFSisNil)
// 500
c.ErrorUseHandle(http.StatusInternalServerError, ErrInputFSisNil)
@ -59,7 +63,7 @@ func FileServerHandleServe(c *Context, fsHandle http.Handler) {
return
} else {
// 否则,返回 405 Method Not Allowed
c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, fmt.Errorf("Method %s is Not Allowed on FileServer", c.Request.Method))
c.engine.errorHandle.handler(c, http.StatusMethodNotAllowed, ErrMethodNotAllowed)
}
} else {
c.Next()
@ -240,7 +244,7 @@ func (engine *Engine) StaticFS(relativePath string, fs http.FileSystem) {
relativePath += "/"
}
fileServer := http.FileServer(fs)
fileServer := http.StripPrefix(relativePath, http.FileServer(fs))
engine.ANY(relativePath+"*filepath", GetStaticFSHandleFunc(fileServer))
}
@ -254,7 +258,7 @@ func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) {
relativePath += "/"
}
fileServer := http.FileServer(fs)
fileServer := http.StripPrefix(relativePath, http.FileServer(fs))
group.ANY(relativePath+"*filepath", GetStaticFSHandleFunc(fileServer))
}

7
go.mod
View file

@ -1,15 +1,16 @@
module github.com/infinite-iroha/touka
go 1.24.5
go 1.25.1
require (
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2
github.com/WJQSERVER-STUDIO/httpc v0.8.2
github.com/WJQSERVER/wanf v0.0.3
github.com/fenthope/reco v0.0.4
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e
)
require (
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/net v0.49.0 // indirect
)

12
go.sum
View file

@ -2,13 +2,13 @@ github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2 h1:AiIHXP21LpK7pFfqUlUstgQEWzjbe
github.com/WJQSERVER-STUDIO/go-utils/iox v0.0.2/go.mod h1:mCLqYU32bTmEE6dpj37MKKiZgz70Jh/xyK9vVbq6pok=
github.com/WJQSERVER-STUDIO/httpc v0.8.2 h1:PFPLodV0QAfGEP6915J57vIqoKu9cGuuiXG/7C9TNUk=
github.com/WJQSERVER-STUDIO/httpc v0.8.2/go.mod h1:8WhHVRO+olDFBSvL5PC/bdMkb6U3vRdPJ4p4pnguV5Y=
github.com/fenthope/reco v0.0.3 h1:RmnQ0D9a8PWtwOODawitTe4BztTnS9wYwrDbipISNq4=
github.com/fenthope/reco v0.0.3/go.mod h1:mDkGLHte5udWTIcjQTxrABRcf56SSdxBOCLgrRDwI/Y=
github.com/WJQSERVER/wanf v0.0.3 h1:OqhG7ETiR5Knqr0lmbb+iUMw9O7re2vEogjVf06QevM=
github.com/WJQSERVER/wanf v0.0.3/go.mod h1:q2Pyg+G+s1acMWxrbI4CwS/Yk76/BzLREEdZ8iFwUNE=
github.com/fenthope/reco v0.0.4 h1:yo2g3aWwdoMpaZWZX4SdZOW7mCK82RQIU/YI8ZUQThM=
github.com/fenthope/reco v0.0.4/go.mod h1:eMyS8HpdMVdJ/2WJt6Cvt8P1EH9Igzj5lSJrgc+0jeg=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 h1:iizUGZ9pEquQS5jTGkh4AqeeHCMbfbjeb0zMt0aEFzs=
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=

184
sse.go Normal file
View file

@ -0,0 +1,184 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright 2025 WJQSERVER. All rights reserved.
// All rights reserved by WJQSERVER, related rights can be exercised by the infinite-iroha organization.
package touka
import (
"bytes"
"io"
"net/http"
"strings"
)
// Event 代表一个服务器发送事件(SSE).
type Event struct {
// Event 是事件的名称.
Event string
// Data 是事件的内容, 可以是多行文本.
Data string
// Id 是事件的唯一标识符.
Id string
// Retry 是指定客户端在连接丢失后应等待多少毫秒后尝试重新连接.
Retry string
}
// Render 将事件格式化并写入给定的 writer.
// 通过逐行处理数据, 此方法可防止因数据中包含换行符而导致的CRLF注入问题.
// 为了性能, 它使用 bytes.Buffer 并通过 WriteTo 直接写入, 以避免不必要的内存分配.
func (e *Event) Render(w io.Writer) error {
var buf bytes.Buffer
if len(e.Id) > 0 {
buf.WriteString("id: ")
buf.WriteString(e.Id)
buf.WriteString("\n")
}
if len(e.Event) > 0 {
buf.WriteString("event: ")
buf.WriteString(e.Event)
buf.WriteString("\n")
}
if len(e.Data) > 0 {
lines := strings.Split(e.Data, "\n")
for _, line := range lines {
buf.WriteString("data: ")
buf.WriteString(line)
buf.WriteString("\n")
}
}
if len(e.Retry) > 0 {
buf.WriteString("retry: ")
buf.WriteString(e.Retry)
buf.WriteString("\n")
}
// 每个事件都以一个额外的换行符结尾.
buf.WriteString("\n")
// 直接将 buffer 的内容写入 writer, 避免生成中间字符串.
_, err := buf.WriteTo(w)
return err
}
// EventStream 启动一个 SSE 事件流.
// 这是推荐的、更简单安全的方式, 采用阻塞和回调的设计, 框架负责管理连接生命周期.
//
// 详细用法:
//
// r.GET("/sse/callback", func(c *touka.Context) {
// // streamer 回调函数会在一个循环中被调用.
// c.EventStream(func(w io.Writer) bool {
// event := touka.Event{
// Event: "time-tick",
// Data: time.Now().Format(time.RFC1123),
// }
//
// if err := event.Render(w); err != nil {
// // 发生写入错误, 停止发送.
// return false // 返回 false 结束事件流.
// }
//
// time.Sleep(2 * time.Second)
// return true // 返回 true 继续事件流.
// })
// // 当事件流结束后(例如客户端关闭页面), 这行代码会被执行.
// fmt.Println("Client disconnected from /sse/callback")
// })
func (c *Context) EventStream(streamer func(w io.Writer) bool) {
// 为现代网络协议优化头部.
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache, no-transform")
c.Writer.Header().Del("Connection")
c.Writer.Header().Del("Transfer-Encoding")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush() // 直接调用, ResponseWriter 接口保证了 Flush 方法的存在.
for {
select {
case <-c.Request.Context().Done():
return
default:
if !streamer(c.Writer) {
return
}
c.Writer.Flush()
}
}
}
// EventStreamChan 返回用于 SSE 事件流的 channel.
// 这是为高级并发场景设计的、更灵活的API.
//
// 重要:
// - 调用者必须 close(eventChan) 来结束事件流.
// - 调用者必须在独立的 goroutine 中消费 errChan 来处理错误和连接断开.
// - 为防止 goroutine 泄漏, 建议发送方在 select 中同时监听 c.Request.Context().Done().
//
// 详细用法:
//
// r.GET("/sse/channel", func(c *touka.Context) {
// eventChan, errChan := c.EventStreamChan()
//
// // 必须在独立的goroutine中处理错误和连接断开.
// go func() {
// if err := <-errChan; err != nil {
// c.Errorf("SSE channel error: %v", err)
// }
// }()
//
// // 在另一个goroutine中异步发送事件.
// go func() {
// // 重要: 必须在逻辑结束时关闭channel, 以通知框架.
// defer close(eventChan)
//
// for i := 1; i <= 5; i++ {
// select {
// case <-c.Request.Context().Done():
// return // 客户端已断开, 退出 goroutine.
// default:
// eventChan <- touka.Event{
// Id: fmt.Sprintf("%d", i),
// Data: "hello from channel",
// }
// time.Sleep(2 * time.Second)
// }
// }
// }()
// })
func (c *Context) EventStreamChan() (chan<- Event, <-chan error) {
eventChan := make(chan Event)
errChan := make(chan error, 1)
c.Writer.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
c.Writer.Header().Set("Cache-Control", "no-cache, no-transform")
c.Writer.Header().Del("Connection")
c.Writer.Header().Del("Transfer-Encoding")
c.Writer.WriteHeader(http.StatusOK)
c.Writer.Flush()
go func() {
defer close(errChan)
for {
select {
case event, ok := <-eventChan:
if !ok {
return
}
if err := event.Render(c.Writer); err != nil {
errChan <- err
return
}
c.Writer.Flush()
case <-c.Request.Context().Done():
errChan <- c.Request.Context().Err()
return
}
}
}()
return eventChan, errChan
}

358
tree.go
View file

@ -2,51 +2,43 @@
// 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框架的核心组件
package touka
import (
"bytes" // 导入 bytes 包,用于操作字节切片
"net/url" // 导入 net/url 包,用于 URL 解析和转义
"strings" // 导入 strings 包,用于字符串操作
"unicode" // 导入 unicode 包,用于处理 Unicode 字符
"unicode/utf8" // 导入 unicode/utf8 包,用于 UTF-8 编码和解码
"unsafe" // 导入 unsafe 包,用于不安全的类型转换,以避免内存分配
"net/url"
"strings"
"unicode"
"unicode/utf8"
"unsafe"
)
// StringToBytes 将字符串转换为字节切片,不进行内存分配。
// 更多详情,请参见 https://github.com/golang/go/issues/53003#issuecomment-1140276077。
// 注意:此函数使用 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 包,应谨慎使用,因为它可能导致内存不安全。
// 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 参数,由键和值组成。
// Param 是单个 URL 参数, 由键和值组成.
type Param struct {
Key string // 参数的键名
Value string // 参数的值
}
// Params 是 Param 类型的切片,由路由器返回。
// 该切片是有序的,第一个 URL 参数也是切片中的第一个值。
// 因此,按索引读取值是安全的。
// Params 是 Param 类型的切片, 由路由器返回.
// 该切片是有序的, 第一个 URL 参数也是切片中的第一个值.
// 因此, 按索引读取值是安全的.
type Params []Param
// Get 返回键名与给定名称匹配的第一个 Param 的值,并返回一个布尔值 true。
// 如果未找到匹配的 Param,则返回空字符串和布尔值 false。
// Get 返回键名与给定名称匹配的第一个 Param 的值, 并返回一个布尔值 true.
// 如果未找到匹配的 Param, 则返回空字符串和布尔值 false.
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
@ -56,24 +48,24 @@ func (ps Params) Get(name string) (string, bool) {
return "", false
}
// ByName 返回键名与给定名称匹配的第一个 Param 的值
// 如果未找到匹配的 Param,则返回空字符串。
// ByName 返回键名与给定名称匹配的第一个 Param 的值.
// 如果未找到匹配的 Param, 则返回空字符串.
func (ps Params) ByName(name string) (va string) {
va, _ = ps.Get(name) // 调用 Get 方法获取值忽略第二个返回值
va, _ = ps.Get(name) // 调用 Get 方法获取值, 忽略第二个返回值
return
}
// methodTree 表示特定 HTTP 方法的路由树
// methodTree 表示特定 HTTP 方法的路由树.
type methodTree struct {
method string // HTTP 方法(例如 "GET", "POST"
method string // HTTP 方法(例如 "GET", "POST")
root *node // 该方法的根节点
}
// methodTrees 是 methodTree 的切片
// methodTrees 是 methodTree 的切片.
type methodTrees []methodTree
// get 根据给定的 HTTP 方法查找并返回对应的根节点
// 如果找不到,则返回 nil。
// get 根据给定的 HTTP 方法查找并返回对应的根节点.
// 如果找不到, 则返回 nil.
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
@ -83,7 +75,7 @@ func (trees methodTrees) get(method string) *node {
return nil
}
// longestCommonPrefix 计算两个字符串的最长公共前缀的长度
// longestCommonPrefix 计算两个字符串的最长公共前缀的长度.
func longestCommonPrefix(a, b string) int {
i := 0
max_ := min(len(a), len(b)) // 找出两个字符串中较短的长度
@ -93,64 +85,61 @@ func longestCommonPrefix(a, b string) int {
return i // 返回公共前缀的长度
}
// addChild 添加一个子节点,并将通配符子节点(如果存在)保持在数组的末尾。
// 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 计算路径中参数(冒号)和捕获所有(星号)的数量。
// 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
colons := strings.Count(path, ":")
stars := strings.Count(path, "*")
return uint16(colons + stars)
}
// countSections 计算路径中斜杠'/')的数量,即路径段的数量。
// countSections 计算路径中斜杠('/')的数量, 即路径段的数量.
func countSections(path string) uint16 {
s := StringToBytes(path) // 将路径字符串转换为字节切片
return uint16(bytes.Count(s, strSlash)) // 统计斜杠的数量
return uint16(strings.Count(path, "/"))
}
// nodeType 定义了节点的类型
// nodeType 定义了节点的类型.
type nodeType uint8
const (
static nodeType = iota // 静态节点路径中不包含参数或通配符
static nodeType = iota // 静态节点, 路径中不包含参数或通配符
root // 根节点
param // 参数节点(例如:name
catchAll // 捕获所有节点(例如*path
param // 参数节点(例如:name)
catchAll // 捕获所有节点(例如*path)
)
// node 表示路由树中的一个节点
// node 表示路由树中的一个节点.
type node struct {
path string // 当前节点的路径段
indices string // 子节点第一个字符的索引字符串用于快速查找子节点
wildChild bool // 是否包含通配符子节点:param 或 *catchAll
nType nodeType // 节点的类型(静态、根、参数、捕获所有)
priority uint32 // 节点的优先级用于查找时优先匹配
children []*node // 子节点切片最多有一个 :param 风格的节点位于数组末尾
indices string // 子节点第一个字符的索引字符串, 用于快速查找子节点
wildChild bool // 是否包含通配符子节点(:param 或 *catchAll)
nType nodeType // 节点的类型(静态, 根, 参数, 捕获所有)
priority uint32 // 节点的优先级, 用于查找时优先匹配
children []*node // 子节点切片, 最多有一个 :param 风格的节点位于数组末尾
handlers HandlersChain // 绑定到此节点的处理函数链
fullPath string // 完整路径用于调试和错误信息
fullPath string // 完整路径, 用于调试和错误信息
}
// incrementChildPrio 增加给定子节点的优先级并在必要时重新排序
// 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]
@ -158,9 +147,9 @@ func (n *node) incrementChildPrio(pos int) int {
// 构建新的索引字符字符串
if newPos != pos {
// 如果位置发生变化则重新构建 indices 字符串
// 如果位置发生变化, 则重新构建 indices 字符串
// 前缀部分 + 移动的索引字符 + 剩余部分
n.indices = n.indices[:newPos] + // 未改变的前缀可能为空
n.indices = n.indices[:newPos] + // 未改变的前缀, 可能为空
n.indices[pos:pos+1] + // 被移动的索引字符
n.indices[newPos:pos] + n.indices[pos+1:] // 除去原位置字符的其余部分
}
@ -168,13 +157,13 @@ func (n *node) incrementChildPrio(pos int) int {
return newPos // 返回新的位置
}
// addRoute 为给定路径添加一个带有处理函数的节点
// 非并发安全
// 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 // 设置为根节点类型
@ -185,12 +174,12 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
walk: // 外部循环用于遍历和构建路由树
for {
// 找到最长公共前缀
// 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符
// 找到最长公共前缀.
// 这也意味着公共前缀不包含 ':' 或 '*',因为现有键不能包含这些字符.
i := longestCommonPrefix(path, n.path)
// 分裂边 (Split edge)
// 如果公共前缀小于当前节点的路径长度说明当前节点需要被分裂
// 如果公共前缀小于当前节点的路径长度, 说明当前节点需要被分裂
if i < len(n.path) {
child := node{
path: n.path[i:], // 子节点路径是当前节点路径的剩余部分
@ -199,27 +188,27 @@ walk: // 外部循环用于遍历和构建路由树
indices: n.indices, // 继承索引
children: n.children, // 继承子节点
handlers: n.handlers, // 继承处理函数
priority: n.priority - 1, // 优先级减1因为分裂会降低优先级
priority: n.priority - 1, // 优先级减1, 因为分裂会降低优先级
fullPath: n.fullPath, // 继承完整路径
}
n.children = []*node{&child} // 当前节点现在只有一个子节点新分裂出的子节点
n.children = []*node{&child} // 当前节点现在只有一个子节点: 新分裂出的子节点
// 将当前节点的 indices 设置为新子节点路径的第一个字符
n.indices = BytesToString([]byte{n.path[i]}) // []byte 用于正确的 Unicode 字符转换
n.path = path[:i] // 当前节点的路径更新为公共前缀
n.handlers = nil // 当前节点不再有处理函数(因为它被分裂了)
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) // 更新父节点完整路径索引
@ -238,8 +227,8 @@ walk: // 外部循环用于遍历和构建路由树
}
}
// 否则插入新节点
// 如果第一个字符不是 ':' 也不是 '*'且当前节点不是 catchAll 类型
// 否则, 插入新节点
// 如果第一个字符不是 ':' 也不是 '*', 且当前节点不是 catchAll 类型
if c != ':' && c != '*' && n.nType != catchAll {
// 将新字符添加到索引字符串
n.indices += BytesToString([]byte{c}) // []byte 用于正确的 Unicode 字符转换
@ -250,18 +239,18 @@ walk: // 外部循环用于遍历和构建路由树
n.incrementChildPrio(len(n.indices) - 1) // 增加新子节点的优先级并重新排序
n = child // 移动到新子节点
} else if n.wildChild {
// 正在插入一个通配符节点需要检查是否与现有通配符冲突
// 正在插入一个通配符节点, 需要检查是否与现有通配符冲突
n = n.children[len(n.children)-1] // 移动到现有的通配符子节点
n.priority++ // 增加其优先级
// 检查通配符是否匹配
// 如果剩余路径长度大于等于通配符节点的路径长度且通配符节点路径是剩余路径的前缀
// 并且不是 catchAll 类型(不能有子路由),
// 如果剩余路径长度大于等于通配符节点的路径长度, 且通配符节点路径是剩余路径的前缀
// 并且不是 catchAll 类型(不能有子路由),
// 并且通配符之后没有更多字符或紧跟着 '/'
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
// 不能向 catchAll 添加子节点
n.nType != catchAll &&
// 检查更长的通配符例如 :name 和 :names
// 检查更长的通配符, 例如 :name 和 :names
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk // 继续外部循环
}
@ -269,7 +258,7 @@ walk: // 外部循环用于遍历和构建路由树
// 通配符冲突
pathSeg := path
if n.nType != catchAll {
pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll则截取到下一个 '/'
pathSeg, _, _ = strings.Cut(pathSeg, "/") // 如果不是 catchAll, 则截取到下一个 '/'
}
prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path // 构造冲突前缀
panic("'" + pathSeg + // 抛出 panic 表示通配符冲突
@ -279,13 +268,13 @@ walk: // 外部循环用于遍历和构建路由树
"'")
}
n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符)
n.insertChild(path, fullPath, handlers) // 插入子节点(可能包含通配符)
return // 完成添加路由
}
// 否则将处理函数添加到当前节点
// 否则, 将处理函数添加到当前节点
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数则报错
panic("handlers are already registered for path '" + fullPath + "'") // 如果已注册处理函数, 则报错
}
n.handlers = handlers // 设置处理函数
n.fullPath = fullPath // 设置完整路径
@ -293,20 +282,20 @@ walk: // 外部循环用于遍历和构建路由树
}
}
// findWildcard 搜索通配符段并检查名称是否包含无效字符
// 如果未找到通配符,则返回 -1 作为索引。
// 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 == ':' { // 如果转义字符是 ':'则跳过
if c == ':' { // 如果转义字符是 ':', 则跳过
continue
}
panic("invalid escape string in path '" + path + "'") // 无效的转义字符串
}
if c == '\\' { // 如果是反斜杠则设置转义标志
if c == '\\' { // 如果是反斜杠, 则设置转义标志
escapeColon = true
continue
}
@ -319,36 +308,36 @@ func findWildcard(path string) (wildcard string, i int, valid bool) {
valid = true // 默认为有效
for end, c := range []byte(path[start+1:]) {
switch c {
case '/': // 如果遇到斜杠说明通配符段结束
case '/': // 如果遇到斜杠, 说明通配符段结束
return path[start : start+1+end], start, valid
case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*'则无效
case ':', '*': // 如果在通配符段中再次遇到 ':' 或 '*', 则无效
valid = false
}
}
return path[start:], start, valid // 返回找到的通配符起始索引和有效性
return path[start:], start, valid // 返回找到的通配符, 起始索引和有效性
}
return "", -1, false // 未找到通配符
}
// insertChild 插入一个带有处理函数的节点
// 此函数处理包含通配符的路径插入逻辑
// insertChild 插入一个带有处理函数的节点.
// 此函数处理包含通配符的路径插入逻辑.
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
for {
// 找到第一个通配符之前的前缀
wildcard, i, valid := findWildcard(path)
if i < 0 { // 未找到通配符结束循环
if i < 0 { // 未找到通配符, 结束循环
break
}
// 通配符名称只能包含一个 ':' 或 '*' 字符
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'") // 报错每个路径段只允许一个通配符
wildcard + "' in path '" + fullPath + "'") // 报错: 每个路径段只允许一个通配符
}
// 检查通配符是否有名称
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错通配符必须有非空名称
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") // 报错: 通配符必须有非空名称
}
if wildcard[0] == ':' { // 如果是参数节点 (param)
@ -368,7 +357,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
n = child // 移动到新创建的参数节点
n.priority++ // 增加优先级
// 如果路径不以通配符结束则会有一个以 '/' 开头的子路径
// 如果路径不以通配符结束, 则会有一个以 '/' 开头的子路径
if len(wildcard) < len(path) {
path = path[len(wildcard):] // 剩余路径去除通配符部分
@ -376,19 +365,19 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
priority: 1, // 新子节点优先级
fullPath: fullPath, // 设置子节点的完整路径
}
n.addChild(child) // 添加子节点(通常是斜杠后的静态部分)
n.addChild(child) // 添加子节点(通常是斜杠后的静态部分)
n = child // 移动到这个新子节点
continue // 继续循环查找下一个通配符或结束
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 + "'") // 报错捕获所有路由只能在路径末尾
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") // 报错: 捕获所有路由只能在路径末尾
}
// 检查路径段冲突
@ -397,34 +386,34 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
if len(n.children) != 0 {
pathSeg, _, _ = strings.Cut(n.children[0].path, "/")
}
panic("catch-all wildcard '" + path + // 报错捕获所有通配符与现有路径段冲突
panic("catch-all wildcard '" + path + // 报错: 捕获所有通配符与现有路径段冲突
"' in new path '" + fullPath +
"' conflicts with existing path segment '" + pathSeg +
"' in existing prefix '" + n.path + pathSeg +
"'")
}
// 当前固定宽度为 1用于 '/'
// 当前固定宽度为 1, 用于 '/'
i--
if i < 0 || path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'") // 报错捕获所有之前没有 '/'
panic("no / before catch-all in path '" + fullPath + "'") // 报错: 捕获所有之前没有 '/'
}
n.path = path[:i] // 当前节点路径更新为 catchAll 之前的部分
// 第一个节点路径为空的 catchAll 节点
// 第一个节点: 路径为空的 catchAll 节点
child := &node{
wildChild: true, // 标记为有通配符子节点
nType: catchAll, // 类型为 catchAll
fullPath: fullPath, // 设置完整路径
}
n.addChild(child) // 添加子节点
n.indices = string('/') // 索引设置为 '/'
n = child // 移动到新创建的 catchAll 节点
n.priority++ // 增加优先级
n.addChild(child) // 添加子节点
n.indices = "/" // 索引设置为 '/'
n = child // 移动到新创建的 catchAll 节点
n.priority++ // 增加优先级
// 第二个节点包含变量的节点
// 第二个节点: 包含变量的节点
child = &node{
path: path[i:], // 路径为 catchAll 的实际路径段
nType: catchAll, // 类型为 catchAll
@ -437,7 +426,7 @@ func (n *node) insertChild(path string, fullPath string, handlers HandlersChain)
return // 完成
}
// 如果没有找到通配符简单地插入路径和处理函数
// 如果没有找到通配符, 简单地插入路径和处理函数
n.path = path // 设置当前节点路径
n.handlers = handlers // 设置处理函数
n.fullPath = fullPath // 设置完整路径
@ -451,16 +440,16 @@ type nodeValue struct {
fullPath string // 匹配到的完整路径
}
// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息,以便回溯。
// skippedNode 结构体用于在 getValue 查找过程中记录跳过的节点信息, 以便回溯.
type skippedNode struct {
path string // 跳过时的当前路径
node *node // 跳过的节点
paramsCount int16 // 跳过时已收集的参数数量
}
// getValue 返回注册到给定路径key的处理函数。通配符的值会保存到 map 中。
// 如果找不到处理函数,则在存在一个带有额外(或不带)尾部斜杠的处理函数时,
// 建议进行 TSR(尾部斜杠重定向)。
// getValue 返回注册到给定路径(key)的处理函数. 通配符的值会保存到 map 中.
// 如果找不到处理函数, 则在存在一个带有额外(或不带)尾部斜杠的处理函数时,
// 建议进行 TSR(尾部斜杠重定向).
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
var globalParamsCount int16 // 全局参数计数
@ -471,11 +460,16 @@ walk: // 外部循环用于遍历路由树
if path[:len(prefix)] == prefix { // 如果路径以当前节点的前缀开头
path = path[len(prefix):] // 移除已匹配的前缀
// 优先尝试所有非通配符子节点,通过匹配索引字符
// 在访问 path[0] 之前进行安全检查
if len(path) == 0 {
continue walk
}
// 优先尝试所有非通配符子节点, 通过匹配索引字符
idxc := path[0] // 剩余路径的第一个字符
for i, c := range []byte(n.indices) {
if c == idxc { // 如果找到匹配的索引字符
// 如果当前节点有通配符子节点,则将当前节点添加到 skippedNodes以便回溯
// 如果当前节点有通配符子节点, 则将当前节点添加到 skippedNodes, 以便回溯
if n.wildChild {
index := len(*skippedNodes)
*skippedNodes = (*skippedNodes)[:index+1]
@ -518,20 +512,20 @@ walk: // 外部循环用于遍历路由树
}
}
// 未找到
// 如果存在一个带有额外(或不带)尾部斜杠的处理函数,
// 我们可以建议重定向到相同 URL,不带尾部斜杠。
value.tsr = path == "/" && n.handlers != nil // 如果路径是 "/" 且当前节点有处理函数则建议 TSR
// 未找到.
// 如果存在一个带有额外(或不带)尾部斜杠的处理函数,
// 我们可以建议重定向到相同 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++
@ -539,7 +533,7 @@ walk: // 外部循环用于遍历路由树
// 保存参数值
if params != nil {
// 如果需要预分配容量
// 如果需要, 预分配容量
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
@ -559,12 +553,12 @@ walk: // 外部循环用于遍历路由树
}
}
(*value.params)[i] = Param{ // 存储参数
Key: n.path[1:], // 参数键名(去除冒号)
Key: n.path[1:], // 参数键名(去除冒号)
Value: val, // 参数值
}
}
// 我们需要继续深入
// 我们需要继续深入!
if end < len(path) {
if len(n.children) > 0 {
path = path[end:] // 移除已提取的参数部分
@ -573,16 +567,16 @@ walk: // 外部循环用于遍历路由树
}
// ... 但我们无法继续
value.tsr = len(path) == end+1 // 如果路径只剩下斜杠则建议 TSR
value.tsr = len(path) == end+1 // 如果路径只剩下斜杠, 则建议 TSR
return value
}
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return value // 如果当前节点有处理函数则返回
return value // 如果当前节点有处理函数, 则返回
}
if len(n.children) == 1 {
// 未找到处理函数。检查是否存在此路径加尾部斜杠的处理函数,以进行 TSR 建议
// 未找到处理函数. 检查是否存在此路径加尾部斜杠的处理函数, 以进行 TSR 建议
n = n.children[0]
value.tsr = (n.path == "/" && n.handlers != nil) || (n.path == "" && n.indices == "/")
}
@ -591,7 +585,7 @@ walk: // 外部循环用于遍历路由树
case catchAll: // 捕获所有节点
// 保存参数值
if params != nil {
// 如果需要预分配容量
// 如果需要, 预分配容量
if cap(*params) < int(globalParamsCount) {
newParams := make(Params, len(*params), globalParamsCount)
copy(newParams, *params)
@ -611,7 +605,7 @@ walk: // 外部循环用于遍历路由树
}
}
(*value.params)[i] = Param{ // 存储参数
Key: n.path[2:], // 参数键名(去除星号)
Key: n.path[2:], // 参数键名(去除星号)
Value: val, // 参数值
}
}
@ -627,7 +621,7 @@ walk: // 外部循环用于遍历路由树
}
if path == prefix { // 如果路径完全匹配当前节点的前缀
// 如果当前路径不等于 '/' 且节点没有注册的处理函数且最近匹配的节点有子节点
// 如果当前路径不等于 '/' 且节点没有注册的处理函数, 且最近匹配的节点有子节点
// 当前节点需要回溯到最后一个有效的 skippedNode
if n.handlers == nil && path != "/" {
for length := len(*skippedNodes); length > 0; length-- {
@ -644,26 +638,26 @@ walk: // 外部循环用于遍历路由树
}
}
}
// 我们应该已经到达包含处理函数的节点
// 检查此节点是否注册了处理函数
// 我们应该已经到达包含处理函数的节点.
// 检查此节点是否注册了处理函数.
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return value // 如果有处理函数则返回
return value // 如果有处理函数, 则返回
}
// 如果此路由没有处理函数,但此路由有通配符子节点,
// 则此路径必须有一个带有额外尾部斜杠的处理函数
// 如果此路由没有处理函数, 但此路由有通配符子节点,
// 则此路径必须有一个带有额外尾部斜杠的处理函数.
if path == "/" && n.wildChild && n.nType != root {
value.tsr = true // 建议 TSR
return value
}
if path == "/" && n.nType == static {
value.tsr = true // 如果是静态节点且路径是根则建议 TSR
value.tsr = true // 如果是静态节点且路径是根, 则建议 TSR
return value
}
// 未找到处理函数。检查此路径加尾部斜杠是否存在处理函数,以进行尾部斜杠重定向建议
// 未找到处理函数. 检查此路径加尾部斜杠是否存在处理函数, 以进行尾部斜杠重定向建议
for i, c := range []byte(n.indices) {
if c == '/' { // 如果索引中包含 '/'
n = n.children[i] // 移动到对应的子节点
@ -676,11 +670,11 @@ walk: // 外部循环用于遍历路由树
return value
}
// 未找到。我们可以建议重定向到相同 URL添加一个额外的尾部斜杠
// 如果该路径的叶节点存在
// 未找到. 我们可以建议重定向到相同 URL, 添加一个额外的尾部斜杠,
// 如果该路径的叶节点存在.
value.tsr = path == "/" || // 如果路径是根路径
(len(prefix) == len(path)+1 && prefix[len(path)] == '/' && // 或者前缀比路径多一个斜杠
path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠且有处理函数
path == prefix[:len(prefix)-1] && n.handlers != nil) // 且路径是前缀去掉最后一个斜杠, 且有处理函数
// 回溯到最后一个有效的 skippedNode
if !value.tsr && path != "/" {
@ -703,17 +697,17 @@ walk: // 外部循环用于遍历路由树
}
}
// findCaseInsensitivePath 对给定路径进行不区分大小写的查找,并尝试找到处理函数。
// 它还可以选择修复尾部斜杠
// 它返回大小写校正后的路径和一个布尔值,指示查找是否成功。
// 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) // 如果路径太长则分配更大的缓冲区
buf = make([]byte, 0, length) // 如果路径太长, 则分配更大的缓冲区
}
ciPath := n.findCaseInsensitivePathRec(
@ -726,7 +720,7 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by
return ciPath, ciPath != nil // 返回校正后的路径和是否成功找到
}
// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节
// shiftNRuneBytes 将字节数组中的字节向左移动 n 个字节.
func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
switch n {
case 0:
@ -742,12 +736,12 @@ func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
}
}
// findCaseInsensitivePathRec 由 n.findCaseInsensitivePath 使用的递归不区分大小写查找函数
// 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 // 保存原始路径
@ -755,13 +749,13 @@ walk: // 外部循环用于遍历路由树
ciPath = append(ciPath, n.path...) // 将当前节点的路径添加到不区分大小写路径中
if len(path) == 0 { // 如果路径已完全匹配
// 我们应该已经到达包含处理函数的节点
// 检查此节点是否注册了处理函数
// 我们应该已经到达包含处理函数的节点.
// 检查此节点是否注册了处理函数.
if n.handlers != nil {
return ciPath // 如果有处理函数则返回校正后的路径
return ciPath // 如果有处理函数, 则返回校正后的路径
}
// 未找到处理函数
// 未找到处理函数.
// 尝试通过添加尾部斜杠来修复路径
if fixTrailingSlash {
for i, c := range []byte(n.indices) {
@ -775,11 +769,11 @@ walk: // 外部循环用于遍历路由树
}
}
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}
// 如果此节点没有通配符(参数或捕获所有)子节点,
// 我们可以直接查找下一个子节点并继续遍历树
// 如果此节点没有通配符(参数或捕获所有)子节点,
// 我们可以直接查找下一个子节点并继续遍历树.
if !n.wildChild {
// 跳过已处理的 rune 字节
rb = shiftNRuneBytes(rb, npLen)
@ -799,9 +793,9 @@ walk: // 外部循环用于遍历路由树
// 处理一个新的 rune
var rv rune
// 查找 rune 的开始位置
// Runes 最长为 4 字节
// -4 肯定会是另一个 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]) {
@ -822,17 +816,17 @@ walk: // 外部循环用于遍历路由树
for i, c := range []byte(n.indices) {
// 小写匹配
if c == idxc {
// 必须使用递归方法因为大写字节和小写字节都可能作为索引存在
// 必须使用递归方法, 因为大写字节和小写字节都可能作为索引存在
if out := n.children[i].findCaseInsensitivePathRec(
path, ciPath, rb, fixTrailingSlash,
); out != nil {
return out // 如果找到则返回
return out // 如果找到, 则返回
}
break
}
}
// 如果未找到匹配项,则对大写 rune 执行相同操作(如果它不同)
// 如果未找到匹配项, 则对大写 rune 执行相同操作(如果它不同)
if up := unicode.ToUpper(rv); up != lo {
utf8.EncodeRune(rb[:], up) // 将大写 rune 编码到缓冲区
rb = shiftNRuneBytes(rb, off)
@ -850,18 +844,18 @@ walk: // 外部循环用于遍历路由树
}
}
// 未找到。我们可以建议重定向到相同 URL不带尾部斜杠
// 如果该路径的叶节点存在
// 未找到. 我们可以建议重定向到相同 URL, 不带尾部斜杠,
// 如果该路径的叶节点存在.
if fixTrailingSlash && path == "/" && n.handlers != nil {
return ciPath // 如果可以修复尾部斜杠且有处理函数则返回
return ciPath // 如果可以修复尾部斜杠且有处理函数, 则返回
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}
n = n.children[0] // 移动到通配符子节点(通常是唯一一个)
n = n.children[0] // 移动到通配符子节点(通常是唯一一个)
switch n.nType {
case param: // 参数节点
// 查找参数结束位置'/' 或路径末尾)
// 查找参数结束位置('/' 或路径末尾)
end := 0
for end < len(path) && path[end] != '/' {
end++
@ -870,7 +864,7 @@ walk: // 外部循环用于遍历路由树
// 将参数值添加到不区分大小写路径中
ciPath = append(ciPath, path[:end]...)
// 我们需要继续深入
// 我们需要继续深入!
if end < len(path) {
if len(n.children) > 0 {
// 继续处理子节点
@ -882,45 +876,45 @@ walk: // 外部循环用于遍历路由树
// ... 但我们无法继续
if fixTrailingSlash && len(path) == end+1 {
return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠则返回
return ciPath // 如果可以修复尾部斜杠且路径只剩下斜杠, 则返回
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
}
if n.handlers != nil {
return ciPath // 如果有处理函数则返回
return ciPath // 如果有处理函数, 则返回
}
if fixTrailingSlash && len(n.children) == 1 {
// 未找到处理函数检查此路径加尾部斜杠是否存在处理函数
// 未找到处理函数. 检查此路径加尾部斜杠是否存在处理函数
n = n.children[0]
if n.path == "/" && n.handlers != nil {
return append(ciPath, '/') // 返回添加斜杠后的路径
}
}
return nil // 未找到返回 nil
return nil // 未找到, 返回 nil
case catchAll: // 捕获所有节点
return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有)
return append(ciPath, path...) // 返回添加剩余路径后的路径(捕获所有)
default:
panic("invalid node type") // 无效的节点类型
}
}
// 未找到
// 未找到.
// 尝试通过添加/删除尾部斜杠来修复路径
if fixTrailingSlash {
if path == "/" {
return ciPath // 如果路径是根路径则返回
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
return nil // 未找到, 返回 nil
}

View file

@ -159,6 +159,7 @@ func TestTreeWildcard(t *testing.T) {
"/doc/go1.html",
"/info/:user/public",
"/info/:user/project/:project",
"/info/:user/project/:project/*filepath",
"/info/:user/project/golang",
"/aa/*xx",
"/ab/*xx",
@ -226,6 +227,7 @@ func TestTreeWildcard(t *testing.T) {
{"/info/gordon/public", false, "/info/:user/public", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}}},
{"/info/gordon/project/golang", false, "/info/:user/project/golang", Params{Param{Key: "user", Value: "gordon"}}},
{"/info/gordon/project/go/src/file.go", false, "/info/:user/project/:project/*filepath", Params{Param{Key: "user", Value: "gordon"}, Param{Key: "project", Value: "go"}, Param{Key: "filepath", Value: "/src/file.go"}}},
{"/aa/aa", false, "/aa/*xx", Params{Param{Key: "xx", Value: "/aa"}}},
{"/ab/ab", false, "/ab/*xx", Params{Param{Key: "xx", Value: "/ab"}}},
{"/a", false, "/:cc", Params{Param{Key: "cc", Value: "a"}}},
@ -1019,3 +1021,58 @@ func TestWildcardInvalidSlash(t *testing.T) {
}
}
}
// TestComplexBacktrackingWithCatchAll 是一个更复杂的回归测试.
// 它确保在静态路径匹配失败后, 路由器能够正确地回溯并成功匹配一个
// 包含多个命名参数、静态部分和捕获所有参数的复杂路由.
// 这个测试对于验证在禁用 RedirectTrailingSlash 时的算法健壮性至关重要.
func TestComplexBacktrackingWithCatchAll(t *testing.T) {
// 1. Arrange: 初始化路由树并设置复杂的路由结构
tree := &node{}
routes := [...]string{
"/abc/b", // 静态诱饵路由
"/abc/:p1/cde", // 一个不相关的、不会被匹配到的干扰路由
"/abc/:p1/:p2/def/*filepath", // 最终应该匹配到的复杂目标路由
}
for _, route := range routes {
tree.addRoute(route, fakeHandler(route))
}
// 2. Act: 执行一个会触发深度回溯的请求
// 这个路径会首先尝试匹配静态的 /abc/b, 但因为后续路径不匹配而失败,
// 从而强制回溯到 /abc/ 节点, 并重新尝试匹配通配符路径.
reqPath := "/abc/b/d/def/some/file.txt"
wantRoute := "/abc/:p1/:p2/def/*filepath"
wantParams := Params{
{Key: "p1", Value: "b"},
{Key: "p2", Value: "d"},
{Key: "filepath", Value: "/some/file.txt"}, // 注意: catch-all 会包含前导斜杠
}
// 使用 defer/recover 来断言整个过程不会发生 panic
defer func() {
if r := recover(); r != nil {
t.Fatalf("预期不应发生 panic, 但在处理路径 '%s' 时捕获到了: %v", reqPath, r)
}
}()
// 执行查找操作
value := tree.getValue(reqPath, getParams(), getSkippedNodes(), false)
// 3. Assert: 验证回溯后的结果是否正确
// 断言找到了一个有效的句柄
if value.handlers == nil {
t.Fatalf("处理路径 '%s' 时句柄不匹配: 期望得到非空的句柄, 但实际为 nil", reqPath)
}
// 断言匹配到了正确的路由
value.handlers[0](nil)
if fakeHandlerValue != wantRoute {
t.Errorf("处理路径 '%s' 时句柄不匹配: \n 得到: %s\n 想要: %s", reqPath, fakeHandlerValue, wantRoute)
}
// 断言URL参数被正确地解析和提取
if value.params == nil || !reflect.DeepEqual(*value.params, wantParams) {
t.Errorf("处理路径 '%s' 时参数不匹配: \n 得到: %v\n 想要: %v", reqPath, *value.params, wantParams)
}
}