From ca03c121d3820586d50803f3f88a415f72505309 Mon Sep 17 00:00:00 2001 From: xfy Date: Fri, 17 Apr 2026 10:11:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(matcher):=20=E6=8F=90=E5=8F=96=20Locat?= =?UTF-8?q?ionType=20=E5=B8=B8=E9=87=8F=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=BD=93=E5=AD=97=E6=AE=B5=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 LocationType 常量定义替代硬编码字符串 - 优化 MatchResult、ExactMatcher、NamedMatcher 结构体字段顺序 - RadixTree.Insert 添加 locationType 参数用于调试追踪 - 更新测试代码适配新接口 Co-Authored-By: Claude Opus 4.7 --- internal/matcher/bench_test.go | 4 +-- internal/matcher/exact.go | 4 +-- internal/matcher/integration_test.go | 2 +- internal/matcher/location.go | 25 ++++---------- internal/matcher/matcher.go | 16 ++++++--- internal/matcher/named.go | 6 ++-- internal/matcher/prefix.go | 2 +- internal/matcher/prefix_priority.go | 2 +- internal/matcher/radix.go | 50 +++++++++++++++------------ internal/matcher/radix_test.go | 51 ++++++++++++++++++++-------- internal/matcher/regex.go | 6 ++-- 11 files changed, 96 insertions(+), 72 deletions(-) diff --git a/internal/matcher/bench_test.go b/internal/matcher/bench_test.go index 99ac2f4..d4ac707 100644 --- a/internal/matcher/bench_test.go +++ b/internal/matcher/bench_test.go @@ -19,7 +19,7 @@ func BenchmarkRadixTree_Insert(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { for _, p := range paths { - tree.Insert(p, handler, i) + tree.Insert(p, handler, i, "prefix") } } } @@ -30,7 +30,7 @@ func BenchmarkRadixTree_Find(b *testing.B) { paths := []string{"/", "/api", "/api/v1", "/api/v2/users/123"} for i, p := range paths { - tree.Insert(p, handler, i+1) + tree.Insert(p, handler, i+1, "prefix") } tree.MarkInitialized() diff --git a/internal/matcher/exact.go b/internal/matcher/exact.go index 3a07160..a34f391 100644 --- a/internal/matcher/exact.go +++ b/internal/matcher/exact.go @@ -6,8 +6,8 @@ import ( // ExactMatcher Hash Map 精确匹配 type ExactMatcher struct { - path string handler fasthttp.RequestHandler + path string priority int } @@ -31,6 +31,6 @@ func (m *ExactMatcher) Result() *MatchResult { Handler: m.handler, Path: m.path, Priority: m.priority, - LocationType: "exact", + LocationType: LocationTypeExact, } } diff --git a/internal/matcher/integration_test.go b/internal/matcher/integration_test.go index 2a3780a..b71a87f 100644 --- a/internal/matcher/integration_test.go +++ b/internal/matcher/integration_test.go @@ -26,7 +26,7 @@ func TestLocationEngine_NginxPriority(t *testing.T) { // 测试 ^~ 阻止正则 result = engine.Match("/api/test.php") - if result.LocationType != "prefix" { + if result.LocationType != "prefix_priority" { t.Errorf("^~ should block regex, got %s", result.LocationType) } } diff --git a/internal/matcher/location.go b/internal/matcher/location.go index ed6b57e..22e3b2d 100644 --- a/internal/matcher/location.go +++ b/internal/matcher/location.go @@ -10,24 +10,13 @@ import ( // LocationEngine 统一匹配引擎 type LocationEngine struct { - // 精确匹配 - Hash Map (O(1)) - exactMatchers map[string]*ExactMatcher - - // 前缀匹配 - Radix Tree (O(log n)) prefixPriorityTree *RadixTree // ^~ 类型(优先级 2) prefixTree *RadixTree // 普通前缀(优先级 4) - - // 正则匹配 - Linear Scan(按配置顺序) - regexMatchers []*RegexMatcher - - // 命名 location - Hash Map - namedMatchers map[string]*NamedMatcher - - // 初始化标记 - initialized bool - - // 冲突检测 - registeredPaths map[string]string + exactMatchers map[string]*ExactMatcher + namedMatchers map[string]*NamedMatcher + registeredPaths map[string]string + regexMatchers []*RegexMatcher + initialized bool } // NewLocationEngine 创建新引擎 @@ -67,7 +56,7 @@ func (e *LocationEngine) AddPrefixPriority(path string, handler fasthttp.Request return err } - return e.prefixPriorityTree.Insert(path, handler, 2) + return e.prefixPriorityTree.Insert(path, handler, 2, "prefix_priority") } // AddRegex 添加正则匹配 location @@ -95,7 +84,7 @@ func (e *LocationEngine) AddPrefix(path string, handler fasthttp.RequestHandler) return err } - return e.prefixTree.Insert(path, handler, 4) + return e.prefixTree.Insert(path, handler, 4, "prefix") } // AddNamed 添加命名 location diff --git a/internal/matcher/matcher.go b/internal/matcher/matcher.go index cc58ece..1bac513 100644 --- a/internal/matcher/matcher.go +++ b/internal/matcher/matcher.go @@ -2,15 +2,23 @@ package matcher import "github.com/valyala/fasthttp" +// LocationType 常量定义 +const ( + LocationTypeExact = "exact" + LocationTypePrefix = "prefix" + LocationTypePrefixPriority = "prefix_priority" + LocationTypeRegex = "regex" + LocationTypeRegexCaseless = "regex_caseless" + LocationTypeNamed = "named" +) + // MatchResult 匹配结果 type MatchResult struct { + Captures map[string]string // 正则捕获组 Handler fasthttp.RequestHandler + LocationType string Path string Priority int - LocationType string - - // 正则捕获组 - Captures map[string]string } // Matcher 接口 diff --git a/internal/matcher/named.go b/internal/matcher/named.go index 0fa3af2..3386e95 100644 --- a/internal/matcher/named.go +++ b/internal/matcher/named.go @@ -4,8 +4,8 @@ import "github.com/valyala/fasthttp" // NamedMatcher @命名 location type NamedMatcher struct { - name string handler fasthttp.RequestHandler + name string } // NewNamedMatcher 创建命名匹配器 @@ -17,7 +17,7 @@ func NewNamedMatcher(name string, handler fasthttp.RequestHandler) *NamedMatcher } // Match 检查命名是否匹配(命名 location 不使用 path 匹配) -func (m *NamedMatcher) Match(path string) bool { +func (m *NamedMatcher) Match(_ string) bool { return false } @@ -27,7 +27,7 @@ func (m *NamedMatcher) Result() *MatchResult { Handler: m.handler, Path: "@" + m.name, Priority: 0, - LocationType: "named", + LocationType: LocationTypeNamed, } } diff --git a/internal/matcher/prefix.go b/internal/matcher/prefix.go index d9fb2f5..f4f6223 100644 --- a/internal/matcher/prefix.go +++ b/internal/matcher/prefix.go @@ -18,7 +18,7 @@ func NewPrefixMatcher() *PrefixMatcher { // AddPath 添加路径 func (pm *PrefixMatcher) AddPath(path string, handler fasthttp.RequestHandler) error { - return pm.tree.Insert(path, handler, pm.priority) + return pm.tree.Insert(path, handler, pm.priority, "prefix") } // Match 前缀匹配,返回最长前缀匹配结果 diff --git a/internal/matcher/prefix_priority.go b/internal/matcher/prefix_priority.go index 421508b..67ce63b 100644 --- a/internal/matcher/prefix_priority.go +++ b/internal/matcher/prefix_priority.go @@ -18,7 +18,7 @@ func NewPrefixPriorityMatcher() *PrefixPriorityMatcher { // AddPath 添加路径 func (ppm *PrefixPriorityMatcher) AddPath(path string, handler fasthttp.RequestHandler) error { - return ppm.tree.Insert(path, handler, ppm.priority) + return ppm.tree.Insert(path, handler, ppm.priority, "prefix_priority") } // Match 前缀优先匹配,返回最长前缀匹配结果 diff --git a/internal/matcher/radix.go b/internal/matcher/radix.go index 538c60c..38de4c9 100644 --- a/internal/matcher/radix.go +++ b/internal/matcher/radix.go @@ -9,11 +9,12 @@ import ( // RadixNode Radix Tree 节点 type RadixNode struct { - prefix string - handler fasthttp.RequestHandler - children []*RadixNode - isLeaf bool - priority int + children []*RadixNode + handler fasthttp.RequestHandler + priority int + isLeaf bool + prefix string + locationType string // exact/prefix/prefix_priority } // RadixTree 前缀匹配 Radix Tree @@ -30,29 +31,31 @@ func NewRadixTree() *RadixTree { } // Insert 插入路径到 Radix Tree(startup-only) -func (t *RadixTree) Insert(path string, handler fasthttp.RequestHandler, priority int) error { +func (t *RadixTree) Insert(path string, handler fasthttp.RequestHandler, priority int, locationType string) error { if t.initialized { return errors.New("RadixTree already initialized") } - return t.insertNode(nil, t.root, path, handler, priority) + return t.insertNode(nil, t.root, path, handler, priority, locationType) } // insertNode 完整路径分割插入算法 -func (t *RadixTree) insertNode(parent *RadixNode, node *RadixNode, path string, handler fasthttp.RequestHandler, priority int) error { +func (t *RadixTree) insertNode(parent *RadixNode, node *RadixNode, path string, handler fasthttp.RequestHandler, priority int, locationType string) error { // Case 1: 空节点(根节点),直接设置 if node.prefix == "" && len(node.children) == 0 && node.handler == nil { if path == "" { node.handler = handler node.priority = priority node.isLeaf = true + node.locationType = locationType return nil } // 创建新子节点 newNode := &RadixNode{ - prefix: path, - handler: handler, - isLeaf: true, - priority: priority, + prefix: path, + handler: handler, + isLeaf: true, + priority: priority, + locationType: locationType, } node.children = append(node.children, newNode) return nil @@ -77,22 +80,24 @@ func (t *RadixTree) insertNode(parent *RadixNode, node *RadixNode, path string, node.handler = handler node.priority = priority node.isLeaf = true + node.locationType = locationType return nil } // 搜索匹配剩余路径的子节点 for _, child := range node.children { if strings.HasPrefix(remaining, child.prefix) { - return t.insertNode(node, child, remaining, handler, priority) + return t.insertNode(node, child, remaining, handler, priority, locationType) } } // 无匹配子节点,创建新子节点 newNode := &RadixNode{ - prefix: remaining, - handler: handler, - isLeaf: true, - priority: priority, + prefix: remaining, + handler: handler, + isLeaf: true, + priority: priority, + locationType: locationType, } node.children = append(node.children, newNode) return nil @@ -110,10 +115,11 @@ func (t *RadixTree) insertNode(parent *RadixNode, node *RadixNode, path string, // 创建新节点保存剩余路径 newNode := &RadixNode{ - prefix: path[commonLen:], - handler: handler, - isLeaf: true, - priority: priority, + prefix: path[commonLen:], + handler: handler, + isLeaf: true, + priority: priority, + locationType: locationType, } // 将原节点和新节点作为 splitNode 的子节点 @@ -159,7 +165,7 @@ func (t *RadixTree) searchLongest(node *RadixNode, path string, bestMatch *Match Handler: node.handler, Path: node.prefix, Priority: node.priority, - LocationType: "prefix", + LocationType: node.locationType, } // nil-safe 优先级比较 + 长度比较 diff --git a/internal/matcher/radix_test.go b/internal/matcher/radix_test.go index 6bd600d..66e9e98 100644 --- a/internal/matcher/radix_test.go +++ b/internal/matcher/radix_test.go @@ -11,7 +11,7 @@ func TestRadixTree_Insert_EmptyNode(t *testing.T) { tree := NewRadixTree() handler := func(ctx *fasthttp.RequestCtx) {} - err := tree.Insert("/api", handler, 1) + err := tree.Insert("/api", handler, 1, "prefix") if err != nil { t.Fatalf("insert failed: %v", err) } @@ -31,8 +31,8 @@ func TestRadixTree_Insert_CommonPrefix(t *testing.T) { handler1 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("1") } handler2 := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("2") } - tree.Insert("/api", handler1, 1) - tree.Insert("/api/users", handler2, 2) + tree.Insert("/api", handler1, 1, "prefix") + tree.Insert("/api/users", handler2, 2, "prefix") result := tree.FindLongestPrefix("/api/users") if result == nil { @@ -53,8 +53,8 @@ func TestRadixTree_Insert_NodeSplit(t *testing.T) { handler1 := func(ctx *fasthttp.RequestCtx) {} handler2 := func(ctx *fasthttp.RequestCtx) {} - tree.Insert("/abc", handler1, 1) - tree.Insert("/abx", handler2, 2) + tree.Insert("/abc", handler1, 1, "prefix") + tree.Insert("/abx", handler2, 2, "prefix") // 应该正确分割 /ab 公共前缀 result := tree.FindLongestPrefix("/abc") @@ -67,9 +67,9 @@ func TestRadixTree_FindLongestPrefix(t *testing.T) { tree := NewRadixTree() handler := func(ctx *fasthttp.RequestCtx) {} - tree.Insert("/", handler, 1) - tree.Insert("/api", handler, 2) - tree.Insert("/api/v1", handler, 3) + tree.Insert("/", handler, 1, "prefix") + tree.Insert("/api", handler, 2, "prefix") + tree.Insert("/api/v1", handler, 3, "prefix") // "/" has priority 1 (wins), "/api" has 2, "/api/v1" has 3 // Lower number = higher priority @@ -86,10 +86,10 @@ func TestRadixTree_Insert_AfterInitialized(t *testing.T) { tree := NewRadixTree() handler := func(ctx *fasthttp.RequestCtx) {} - tree.Insert("/api", handler, 1) + tree.Insert("/api", handler, 1, "prefix") tree.MarkInitialized() - err := tree.Insert("/api/v2", handler, 2) + err := tree.Insert("/api/v2", handler, 2, "prefix") if err == nil { t.Error("should fail when inserting after initialized") } @@ -99,8 +99,8 @@ func TestRadixTree_Insert_DuplicatePath(t *testing.T) { tree := NewRadixTree() handler := func(ctx *fasthttp.RequestCtx) {} - tree.Insert("/api", handler, 1) - err := tree.Insert("/api", handler, 2) + tree.Insert("/api", handler, 1, "prefix") + err := tree.Insert("/api", handler, 2, "prefix") if err == nil { t.Error("should fail on duplicate path") } @@ -110,7 +110,7 @@ func TestRadixTree_FindLongestPrefix_NoMatch(t *testing.T) { tree := NewRadixTree() handler := func(ctx *fasthttp.RequestCtx) {} - tree.Insert("/api", handler, 1) + tree.Insert("/api", handler, 1, "prefix") result := tree.FindLongestPrefix("/other") if result != nil { @@ -123,8 +123,8 @@ func TestRadixTree_PriorityComparison(t *testing.T) { h1 := func(ctx *fasthttp.RequestCtx) {} h2 := func(ctx *fasthttp.RequestCtx) {} - tree.Insert("/api", h1, 5) - tree.Insert("/api/users", h2, 2) + tree.Insert("/api", h1, 5, "prefix") + tree.Insert("/api/users", h2, 2, "prefix") // Lower priority number wins result := tree.FindLongestPrefix("/api/users") @@ -135,3 +135,24 @@ func TestRadixTree_PriorityComparison(t *testing.T) { t.Errorf("expected priority 2, got %d", result.Priority) } } + +func TestRadixTree_Insert_ExactMatch(t *testing.T) { + // Case 3: path 完全匹配节点前缀,设置 handler + tree := NewRadixTree() + handler := func(ctx *fasthttp.RequestCtx) { ctx.SetBodyString("exact") } + + // 先插入父路径 + tree.Insert("/api", handler, 1, "prefix") + + // 再次插入相同路径(应该报错重复) + err := tree.Insert("/api", handler, 2, "prefix") + if err == nil { + t.Error("should return error for duplicate path") + } + + // 验证原 handler 未被覆盖 + result := tree.FindLongestPrefix("/api") + if result == nil || result.Priority != 1 { + t.Errorf("original handler should not be overwritten, got priority %d", result.Priority) + } +} diff --git a/internal/matcher/regex.go b/internal/matcher/regex.go index 008e7bb..1ceb702 100644 --- a/internal/matcher/regex.go +++ b/internal/matcher/regex.go @@ -10,9 +10,9 @@ import ( type RegexMatcher struct { pattern *regexp.Regexp handler fasthttp.RequestHandler + captures map[string]string priority int caseInsensitive bool - captures map[string]string } // NewRegexMatcher 创建正则匹配器 @@ -47,9 +47,9 @@ func (m *RegexMatcher) Match(path string) bool { // Result 返回匹配结果 func (m *RegexMatcher) Result() *MatchResult { - locType := "regex" + locType := LocationTypeRegex if m.caseInsensitive { - locType = "regex_caseless" + locType = LocationTypeRegexCaseless } return &MatchResult{ Handler: m.handler,