# Handler路由树

此前我们使用map实现了我们的路由,这种形式下,路由匹配并不能满足我们一些路由需求,如:/user/:id/user/* 等一些特殊的路由匹配

# 版本一[基础版本]

# 路由树

我们使用前缀树(字典树)的原理去实现我们的路由树,我们只需要实现我们之前定好的handler接口即可。

package main

import (
	"net/http"
	"strings"
)

func NewHandlerBaseOnTree()	Handler {
	return &HandlerBaseOnTree{
		root: NewNode(), // 初始化树的根节点
	}
}

type HandlerBaseOnTree struct {
	root *Node
}

func NewNode() *Node {
	return &Node{
		next: make([]*Node, 0, 2),
	}
}

func NewNodeByPath(path string) *Node {
	return &Node{
		path: path,
		next: make([]*Node, 0, 2),
	}
}

type Node struct {
	path string
	next []*Node

	// 可以使用以下结构加速
	// next map[byte][]*Node  使用map以不用字母开头分组
	// next [byte][]*Node     使用数组以不用字母开头分组
	// next []*Node           采用有序的接口进行二分法查找

	handleFunc HandleFunc
}

func (h *HandlerBaseOnTree) ServeHTTP(ctx *Context) {
	path := strings.Trim(ctx.R.URL.Path, "/")
	handleFunc, ok := h.FindHandleFunc(path)
	if !ok {
		_ = ctx.WriteJSON(http.StatusNotFound, handleFunc)
		return
	}
	handleFunc(ctx)
}

func (h *HandlerBaseOnTree) FindHandleFunc(path string) (HandleFunc, bool) {
	paths := strings.Split(path, "/")
	cur := h.root
	for _, p := range paths {
		child, ok := h.findCurChild(cur, p)
		if ok {
			cur = child
		} else {
			return nil, false
		}
	}
	return cur.handleFunc, cur.handleFunc != nil
}

// method 当前版本为无用参数,仅仅走路由匹配
func (h *HandlerBaseOnTree) Route(method string, pattern string, handleFunc func(*Context)) {
	pattern = strings.Trim(pattern, "/")
	paths := strings.Split(pattern, "/")
	cur := h.root
	for _, path := range paths {
		child, ok := h.findCurChild(cur, path)
		if ok {
			cur = child
		} else {
			newChild := NewNodeByPath(path)
			cur.next = append(cur.next, newChild)
			cur = newChild
		}
	}
	cur.handleFunc = handleFunc
}

// 寻找节点子节点
func (h *HandlerBaseOnTree) findCurChild(cur *Node, path string) (*Node, bool) {
	for _, node := range cur.next {
		if node.path == path {
			return node, true
		}
	}
	return nil, false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

# 创建Server

func NewServer(name string, builders... FilterBuilder) Server {
	handler := NewHandlerBaseOnTree()

	var root Filter = handler.ServeHTTP

	for i:=len(builders)-1; i>=0; i-- {
		b := builders[i]
		root = b(root)
	}

	return &SDKHTTPServer{
		Name: name,
		handler: handler,
		root: root,
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 版本二[支持通配符]

# 路由树

修改查找时的匹配规则,任何数据遇到通配符将节点返回

func (h *HandlerBaseOnTree) findCurChild(cur *Node, path string) (*Node, bool) {
	for _, node := range cur.next {
		// 并不是 * 的节点命中了,直接返回
		// != * 是为了防止用户乱输入
		if node.path == path && node.path != "*" {
			return node, true
		}
		if node.path == "*" { // 通配符节点
			return node, true
		}
	}
	return nil, false
}
1
2
3
4
5
6
7
8
9
10
11
12
13

增加校验逻辑,不允许配置了通配符的路由,通配符不在最后一个位置:

func (h *HandlerBaseOnTree) validatePattern(pattern string) error {
	// 校验 *,如果存在,必须在最后一个,并且它前面必须是/
	// 即我们只接受 /* 的存在,abc*这种是非法

	pos := strings.Index(pattern, "*")
	// 找到了 *
	if pos > 0 {
		// 必须是最后一个
		if pos != len(pattern) - 1 {
			return ErrorInvalidRouterPattern
		}
		if pattern[pos-1] != '/' {
			return ErrorInvalidRouterPattern
		}
	}
	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

修改Route函数,并修改接口,将错误返回:

func (h *HandlerBaseOnTree) Route(method string, pattern string, handleFunc func(*Context)) error {
	// 校验通配符的位置,只允许通配符出现在最后一个位置
	err := h.validatePattern(pattern)
	if err != nil {
		return err
	}

	pattern = strings.Trim(pattern, "/")
	paths := strings.Split(pattern, "/")
	cur := h.root
	for _, path := range paths {
		child, ok := h.findCurChild(cur, path)
		if ok {
			cur = child
		} else {
			newChild := NewNodeByPath(path)
			cur.next = append(cur.next, newChild)
			cur = newChild
		}
	}
	cur.handleFunc = handleFunc
	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 版本三[支持参数]

我们之前的匹配机制,写在findCurChild函数中,如果继续往下写的话,逻辑会越来越多,经过对路由树业务的理解,我们发现不仅仅只有两种节点,节点之间时不一样的,匹配机制也是不一样的,此时我们可以将我们的逻辑抽象到节点当中,匹配机制的匹配函数也可以抽象出来。

# 节点抽象

于是我们可以抽象出如下节点:参数节点、通配符节点、固定路径节点

package main

import "strings"

const (
	// 根节点,只有根用这个
	nodeTypeRoot = iota

	// *
	nodeTypeAny

	// 路径参数
	nodeTypeParam

	// 正则
	nodeTypeReg

	// 静态,即完全匹配
	nodeTypeStatic
)

const NodePathAny = "*"

type NodeType int

type MatchFunc  func(path string, ctx *Context) bool

type Node struct {
	path string

	next []*Node
	// 可以使用以下结构加速
	// 1. next map[byte][]*Node  使用map以不用字母开头分组
	// 2. next [26][]*Node       使用数组以不用字母开头分组
	// 		                     第一个字符 - 'a',不同字母开头位置进行区分
	// 3. next []*Node           采用有序的接口进行二分法查找

	handleFunc HandleFunc
	matchFunc  MatchFunc
	nodeType   NodeType
}

func NewNode(path string) *Node{
	if path == NodePathAny {
		return newAnyNode(path)
	}
	if strings.HasPrefix(path, ":") {
		return newParamNode(path)
	}
	return newStaticNode(path)
}

// 静态节点
func newStaticNode(path string) *Node {
	return &Node{
		next: make([]*Node, 0, 2),
		matchFunc: func(p string, c *Context) bool {
			return path == p && p != "*"
		},
		nodeType: nodeTypeStatic,
		path:  path,
	}
}

func newRootNode(method string) *Node {
	return &Node{
		next: make([]*Node, 0, 2),
		matchFunc: func( p string, c *Context) bool {
			panic(any("never call me"))
		},
		nodeType: nodeTypeRoot,
		path:  method,
	}
}

func newAnyNode(path string) *Node {
	return &Node{
		// 因为我们不允许 * 后面还有节点,所以这里可以不用初始化
		//children: make([]*node, 0, 2),
		next: make([]*Node, 0, 2),
		matchFunc: func(p string, c *Context) bool {
			if p != "*" {
				return true
			}
			return false
		},
		nodeType: nodeTypeAny,
		path:  path,
	}
}

func newParamNode(path string) *Node {
	param := path[1:]
	return &Node{
		// 因为我们不允许 * 后面还有节点,所以这里可以不用初始化
		//children: make([]*node, 0, 2),
		next: make([]*Node, 0, 2),
		matchFunc: func(p string, c *Context) bool {
			if c != nil && c.PathParams != nil {
				c.PathParams[param] = p
			}
			return p != NodePathAny
		},
		nodeType: nodeTypeParam,
		path:  path,
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107

切片查找加速的几种方式:

	next []*Node
	// 可以使用以下结构加速
	// 1. next map[byte][]*Node  使用map以不用字母开头分组
	// 2. next [26][]*Node       使用数组以不用字母开头分组
	// 		                     第一个字符 - 'a',不同字母开头位置进行区分
	// 3. next []*Node           采用有序的接口进行二分法查找
1
2
3
4
5
6

# 路由树修改

// 初始化支持各种方法
func NewHandlerBaseOnTree()	Handler {
	forest := make(map[string]*Node, len(supportMethods))
	for _, method := range supportMethods {
		forest[method] = newRootNode(method)
	}
	return &HandlerBaseOnTree{
		forest: forest,
	}
}

var supportMethods = [4]string {
	http.MethodGet,
	http.MethodPost,
	http.MethodPut,
	http.MethodDelete,
}

// 修改查找函数,添加方法,找对应的方法的节点
func (h *HandlerBaseOnTree) FindHandleFunc(method, path string) (HandleFunc, bool) {
	paths := strings.Split(path, "/")
	cur := h.forest[method]
	for _, p := range paths {
		child, ok := h.findCurChild(cur, p)
		if ok {
			cur = child
		} else {
			return nil, false
		}
	}
	return cur.handleFunc, cur.handleFunc != nil
}

// 路由函数也对应新增节点
func (h *HandlerBaseOnTree) Route(method string, pattern string, handleFunc func(*Context)) error {
	// 校验通配符的位置,只允许通配符出现在最后一个位置
	err := h.validatePattern(pattern)
	if err != nil {
		return err
	}

	pattern = strings.Trim(pattern, "/")
	paths := strings.Split(pattern, "/")
	cur := h.forest[method]
	for _, path := range paths {
		child, ok := h.findCurChild(cur, path)
		if ok {
			cur = child
		} else {
			newChild := NewNodeByPath(path)
			cur.next = append(cur.next, newChild)
			cur = newChild
		}
	}
	cur.handleFunc = handleFunc
	return nil
}


// 新建节点工厂函数
func NewNode(path string) *Node{
	if path == NodePathAny {
		return newAnyNode(path)
	}
	if strings.HasPrefix(path, ":") {
		return newParamNode(path)
	}
	return newStaticNode(path)
}

// 多中节点优先级不同可能会冲突,新增判断逻辑
func (h *HandlerBaseOnTree) Route(method string, pattern string, handleFunc func(*Context)) error {
	// 校验通配符的位置,只允许通配符出现在最后一个位置
	err := h.validatePattern(pattern)
	if err != nil {
		return err
	}

	pattern = strings.Trim(pattern, "/")
	paths := strings.Split(pattern, "/")
	cur := h.forest[method]
	for _, path := range paths {
		child, ok := h.findMatchChild(cur, path, nil)
		// != nodeTypeAny 是考虑到 /order/* 和 /order/:id 这种注册顺序
		if ok && child.nodeType != nodeTypeAny {
			cur = child
		} else {
			// 为当前节点根据
			newChild := NewNode(path)
			cur.next = append(cur.next, newChild)
			cur = newChild
		}
	}
	cur.handleFunc = handleFunc
	return nil
}

// 修改匹配函数,新增ctx参数,用于参数节点设置参数,并将匹配函数提到节点中
func (h *HandlerBaseOnTree) findMatchChild(cur *Node, path string, ctx *Context) (*Node, bool) {
	candidates := make([]*Node, 0, 2)
	for _, node := range cur.next {
		if node.matchFunc(path, ctx) {
			candidates = append(candidates, node)
		}
	}

	if len(candidates) == 0 {
		return nil, false
	}
	// type 也决定了它们的优先级
	sort.Slice(candidates, func(i, j int) bool {
		return candidates[i].nodeType < candidates[j].nodeType
	})
	return candidates[len(candidates) - 1], true
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115

# Context修改

type Context struct {
	W          http.ResponseWriter
	R          *http.Request
	PathParams map[string]string  // 新增路径参数map,用于记录参数节点的参数
}

func NewContext(w http.ResponseWriter, r *http.Request) *Context {
	return &Context{
		W: w,
		R: r,
		PathParams: map[string]string{}, // 初始化节点
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 拓展

在之前的基础上,我们还可以做一些抽象,让用户自定义如:

  • 节点优先级的选择可以抽象为一个接口,让用户自己决定优先级
  • 支持用户自定义节点,使用自己的匹配规则

# 节点自定义

注册工厂

优点:

  • 支持自定义工厂,工厂方便拓展

缺点:

  • 如何选工厂比较麻烦
type Factory func() *Node

var factories map[int]Factory

func RegisterFactory(factoryType int, factory Factory) {
	factories[factoryType]= factory
}
1
2
3
4
5
6
7

拓展原工厂

优点:

  • 较容易实现

缺点:

  • 需要提供详细的文档及示例

// 原来的工厂函数
func NewNode(path string) *Node{
	if path == NodePathAny {
		return newAnyNode(path)
	}
	if strings.HasPrefix(path, ":") {
		return newParamNode(path)
	}
	return newStaticNode(path)
}

// 拓展原工厂
type Factory2 func(path string) *Node

var factory Factory2

func RegisterFactory2(f Factory2) {
	factory = f
}

func ExampleFactory2() {
	RegisterFactory2(func(path string) *Node {
		if path == "自定义的path" {
			return &Node{path: "创建自定义的节点"}
		} else {
			return NewNode(path)
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 默认参数 option 模式

package main

import "fmt"

type User struct {
	// 必填
	Id string
	Name string

	// 可选参数
	Address string
}

type Option func(u *User)

// 采用 option 设置可选参数
func WithUserAddress(address string) Option {
	return func (user *User) {
		user.Address = address
	}
}

func NewUser(id, name string, options ...Option) *User  {
	user := &User{Id: id, Name: name}

	for _, o := range options {
		o(user)
	}

	return user
}

func main() {
	user := NewUser("123", "xiao ming", WithUserAddress("深圳"))
	fmt.Println(user)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# Filter配置化

自定义注册 filter builder

 var builderMap map[string]FilterBuilder
func RegisterFilterBuilder (name string, filterBuilder FilterBuilder) {
	builderMap[name] = filterBuilder
}

func GetFilterBuilders(names ... string) []FilterBuilder {
	filterBuilders := make([]FilterBuilder, 0, len(names))

	for _, name := range names {
		filterBuilders = append(filterBuilders, builderMap[name])
	}

	return filterBuilders
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

初始化服务使用builder的名称

func NewServerWithBuilderNames(serverName string, builderNames ...string) Server {
	// builderNames 可以从配置文件中获取
	// 配置文件中可这么配置:filter: "logFilter, panicRecoverFilter, RegisterFilter"
	return NewServer(serverName, GetFilterBuilders(builderNames...)...)
}
1
2
3
4
5

初始化示例

func main(){
	// 读取配置,切割得到字符串切片
	configBuilders := []string{"logFilter", "recoverFilter"}
	server := web.NewServerWithBuilderNames("my-server", configBuilders...)
	_ = server.Start(":8888")
}
1
2
3
4
5
6
上次更新: 12/26/2023,