refactor(matcher): 提取 LocationType 常量并优化结构体字段布局

- 添加 LocationType 常量定义替代硬编码字符串
- 优化 MatchResult、ExactMatcher、NamedMatcher 结构体字段顺序
- RadixTree.Insert 添加 locationType 参数用于调试追踪
- 更新测试代码适配新接口

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
xfy 2026-04-17 10:11:45 +08:00
parent c418baeb90
commit ca03c121d3
11 changed files with 96 additions and 72 deletions

View File

@ -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()

View File

@ -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,
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 接口

View File

@ -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,
}
}

View File

@ -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 前缀匹配,返回最长前缀匹配结果

View File

@ -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 前缀优先匹配,返回最长前缀匹配结果

View File

@ -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 Treestartup-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 优先级比较 + 长度比较

View File

@ -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)
}
}

View File

@ -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,