> For the complete documentation index, see [llms.txt](https://go.sai.show/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://go.sai.show/part03.-lu-you-shu/3.11-lu-you-shu-can-shu-lu-jing-zhi-can-shu-zhi.md).

# 3.11 路由树-参数路径之参数值

本节课工程结构如下:

```
(base) yanglei@yuanhong 16-valueOfParamRoute % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 9 files
```

## PART1. 获取参数值

之前已经实现了参数路径的注册与查找,但仍有一个核心问题没有解决:**我们没有办法将参数从路由中带到逻辑处理函数中**.

### 1.1 定义新类型

我们现在定义的`node`结构体是不包含参数路径中的参数名和参数值的.因此我们需要新定义一个类型,该类型除了表示命中的路由节点外,还包含该节点的参数名和参数值(如果该节点是参数路径节点的话)

`matchNode.go`:

```go
package valueOfParamRoute

// matchNode 用于保存匹配到的节点与路径参数
type matchNode struct {
	// node 匹配到的节点
	node *node
	// pathParams 路径参数 若该节点不是参数节点,则该字段值为nil
	pathParams map[string]string
}
```

注:这里我认为代码中出现`Info`之类的指代意义不明的单词不太好,因此将老师上课时命名的`matchInfo`修改为了`matchNode`.

### 1.2 修改`chlidOf()`方法

**该方法需要再返回一个标识该节点是否为参数路径节点的标量**

`node.go`:

```go
// childOf 根据给定的path在当前节点的子节点映射中查找对应的子节点(即:匹配到了静态路由)
// 若未在子节点映射中找到对应子节点 则先尝试返回当前节点的参数路由子节点(即:匹配到了参数路由)
// 若参数路由子节点为空 则尝试返回当前节点的通配符子节点(即:匹配到了通配符路由)
// 优先级: 静态路由 > 参数路由 > 通配符路由
// child: 查找到的子节点
// isParamChild: 查找到的子节点是否为参数路由子节点
// found: 是否找到了对应的子节点
func (n *node) childOf(path string) (child *node, isParamChild bool, found bool) {
	// 当前节点的子节点映射为空 则有可能匹配到 参数路由子节点 或通配符子节点
	// 此处优先查找参数路由子节点 因为参数路由子节点更具体 所以参数路由的优先级高于通配符路由
	if n.children == nil {
		// 如果当前节点的参数子节点不为空 则尝试返回当前节点的参数子节点
		if n.paramChild != nil {
			return n.paramChild, true, true
		}

		// 如果当前节点的参数子节点为空 则尝试返回当前节点的通配符子节点
		return n.wildcardChild, false, n.wildcardChild != nil
	}

	// 在子当前节点的节点映射中查找对应的子节点 若未找到同样尝试返回当前节点的参数子节点
	// 若参数子节点为空 则尝试返回当前节点的通配符子节点
	child, found = n.children[path]
	if !found {
		if n.paramChild != nil {
			return n.paramChild, true, true
		}
		return n.wildcardChild, false, n.wildcardChild != nil
	}

	// 找到了对应的子节点 则返回该子节点
	return child, false, found
}
```

### 1.3 修改`findRoute()`方法

#### 1.3.1 为`matchNode`结构体新增方法

`matchNode.go`:新增一个用于添加路径参数的方法`addPathParams()`

```go
package valueOfParamRoute

// matchNode 用于保存匹配到的节点与路径参数
type matchNode struct {
	// node 匹配到的节点
	node *node
	// pathParams 路径参数 若该节点不是参数节点,则该字段值为nil
	pathParams map[string]string
}

// addPathParams 用于添加路径参数
func (m *matchNode) addPathParams(name string, value string) {
	// Tips: 这里作为框架的设计者 你是没法确定用户会注册多少个参数路由的
	// Tips: 因此给不给容量意义不大
	if m.pathParams == nil {
		m.pathParams = map[string]string{}
	}
	m.pathParams[name] = value
}
```

#### 1.3.2 修改`findRoute()`方法

这里`findRoute()`方法就不能再返回`node`了,因为需要将参数名/值和节点一起返回.因此需要返回`matchNode`类型.

既然`chlidOf()`方法已经返回了表达当前节点是否为参数路径节点的标量,那么`findRoute()`方法需要做的事情就是:**若当前节点为参数路径节点,则将参数名和参数值一同返回**

`router.go`:

```go
// findRoute 根据给定的HTTP方法和路由路径,在路由森林中查找对应的节点
// 若该节点为参数路径节点,则不仅返回该节点,还返回参数名和参数值
// 否则,仅返回该节点
func (r *router) findRoute(method string, path string) (*matchNode, bool) {
	targetMatchNode := &matchNode{}
	root, ok := r.trees[method]
	// 给定的HTTP动词在路由森林中不存在对应的路由树,则直接返回false
	if !ok {
		return nil, false
	}

	// 对根节点做特殊处理
	if path == "/" {
		targetMatchNode.node = root
		return targetMatchNode, true
	}

	// 给定的HTTP动词在路由森林中存在对应的路由树,则在该路由树中查找对应的节点
	// 去掉前导和后置的"/"
	path = strings.Trim(path, "/")
	segments := strings.Split(path, "/")

	// Tips: 同样的 这里我认为用target作为变量名表现力更强
	target := root

	for _, segment := range segments {
		child, isParamChild, found := target.childOf(segment)
		// 如果在当前节点的子节点映射中没有找到对应的子节点,则直接返回
		if !found {
			return nil, false
		}

		// 若当前节点为参数节点,则将参数名和参数值保存到targetMatchNode中
		if isParamChild {
			// 参数名是形如 :id 的格式, 因此需要去掉前导的:
			name := child.path[1:]
			// 参数值就是当前路由路径中的路由段
			value := segment
			targetMatchNode.addPathParams(name, value)
		}

		// 如果在当前节点的子节点映射中找到了对应的子节点,则继续在该子节点中查找
		target = child
	}

	// 如果找到了对应的节点,则返回该节点
	// Tips: 此处有2种设计 一种是用标量表示是否找到了子节点
	// Tips: 另一种是 return target, target.HandleFunc != nil
	// Tips: 这种返回就表示找到了子节点且子节点必然有对应的业务处理函数
	// 此处我倾向用第1种设计 因为方法名叫findRoute,表示是否找到节点的意思.而非表示是否找到了一个有对应的业务处理函数的节点
	targetMatchNode.node = target
	return targetMatchNode, true
}
```

## PART2. 测试

### 2.1 修改已有的测试用例

#### 2.1.1 修改路由查找功能的测试用例

这个用例其实并没有运行,就是为了编译通过而修改.因为这个用例里没有测试参数路径匹配.

`router_test.go`:

```go
// TestRouter_findRoute 测试路由查找功能
func TestRouter_findRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// GET方法路由树
		TestNode{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		TestNode{
			method: http.MethodGet,
			path:   "/",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}

	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 构造测试用例
	testCases := []struct {
		name      string
		method    string
		path      string
		isFound   bool
		matchNode *matchNode
	}{
		// 测试HTTP动词不存在的用例
		{
			name:      "method not found",
			method:    http.MethodDelete,
			path:      "/user",
			isFound:   false,
			matchNode: nil,
		},

		// 测试完全命中的用例
		{
			name:    "order detail",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			matchNode: &matchNode{
				node: &node{
					path:       "detail",
					children:   nil,
					HandleFunc: mockHandleFunc,
				},
			},
		},

		// 测试命中了节点但节点的HandleFunc为nil的情况
		{
			name:    "order",
			method:  http.MethodGet,
			path:    "/order",
			isFound: true,
			matchNode: &matchNode{
				node: &node{
					path: "order",
					children: map[string]*node{
						"detail": &node{
							path:       "detail",
							children:   nil,
							HandleFunc: mockHandleFunc,
						},
					},
					HandleFunc: nil,
				},
			},
		},

		// 测试根节点
		{
			name:    "",
			method:  http.MethodGet,
			path:    "/",
			isFound: true,
			matchNode: &matchNode{
				node: &node{
					path: "/",
					children: map[string]*node{
						"order": &node{
							path: "order",
							children: map[string]*node{
								"detail": &node{
									path:       "detail",
									children:   nil,
									HandleFunc: mockHandleFunc,
								},
							},
							HandleFunc: nil,
						},
					},
					HandleFunc: mockHandleFunc,
				},
			},
		},

		// 测试路由不存在的用例
		{
			name:      "path not found",
			method:    http.MethodGet,
			path:      "/user",
			isFound:   false,
			matchNode: nil,
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			foundNode, found := r.findRoute(testCase.method, testCase.path)
			// Tips: testCase.isFound是期望的结果,而found是实际的结果
			assert.Equal(t, testCase.isFound, found)

			// 没有找到路由就不用继续比较了
			if !found {
				return
			}

			// 此处和之前的测试一样 不能直接用assert.Equal()比较 因为HandleFunc不可比
			// 所以要用封装的node.equal()方法比较
			msg, found := testCase.matchNode.node.equal(foundNode.node)
			assert.True(t, found, msg)
		})
	}
}
```

#### 2.1.2 通配符路由查找功能的测试用例

这个用例其实并没有运行,就是为了编译通过而修改.因为这个用例里也没有测试参数路径匹配.

`router_test.go`:

```go
// TestRouter_findRoute_wildcard 测试针对通配符路由的查找功能
func TestRouter_findRoute_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}

	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 构造测试用例
	testCases := []struct {
		name      string
		method    string
		path      string
		isFound   bool
		matchNode *matchNode
	}{
		// 普通节点的通配符子节点测试用例
		{
			name:    "order wildcard",
			method:  http.MethodGet,
			path:    "/order/abc",
			isFound: true,
			matchNode: &matchNode{
				node: &node{
					path:          "*",
					children:      nil,
					wildcardChild: nil,
					HandleFunc:    mockHandleFunc,
				},
			},
		},
		// 普通节点下普通子节点和通配符子节点共存的测试用例
		{
			name:    "order detail",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			matchNode: &matchNode{
				node: &node{
					path:          "detail",
					children:      nil,
					wildcardChild: nil,
					HandleFunc:    mockHandleFunc,
				},
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			foundNode, found := r.findRoute(testCase.method, testCase.path)
			assert.Equal(t, testCase.isFound, found)

			if !found {
				return
			}

			msg, found := testCase.matchNode.node.equal(foundNode.node)
			assert.True(t, found, msg)
		})
	}
}
```

#### 2.1.3 针对参数路由查找功能的测试用例

```go
// TestRouter_findRoute_param 测试针对参数路由的查找功能
func TestRouter_findRoute_param(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/order/detail/:id",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}

	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 构造测试用例
	testCases := []struct {
		name     string
		method   string
		path     string
		isFound  bool
		matchNode *matchNode
	}{
		// 普通节点的参数路由子节点测试用例
		{
			name:    "order detail id",
			method:  http.MethodGet,
			path:    "/order/detail/123",
			isFound: true,
			matchNode: &matchNode{
				node: &node{
					path:          ":id",
					children:      nil,
					wildcardChild: nil,
					paramChild:    nil,
					HandleFunc:    mockHandleFunc,
				},
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			foundNode, found := r.findRoute(testCase.method, testCase.path)
			assert.Equal(t, testCase.isFound, found)

			if !found {
				return
			}

			msg, found := testCase.matchNode.node.equal(foundNode.node)
			assert.True(t, found, msg)
		})
	}
}
```

#### 2.1.4 修改`serve()`方法

因为`findRoute()`方法的返回值有变动而造成的修改.

```go
// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context) {
	method := ctx.Req.Method
	path := ctx.Req.URL.Path
	targetNode, ok := s.findRoute(method, path)
	// 没有在路由树中找到对应的路由节点 或 找到了路由节点的处理函数为空(即NPE:none pointer exception 的问题)
	// 则返回404
	if !ok || targetNode.node.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		// 此处确实会报错 但是作为一个WEB框架 遇上了这种错误也没有特别好的处理办法
		// 最多只能是落个日志
		_, _ = ctx.Resp.Write([]byte("Not Found"))
		return
	}

	// 执行路由节点的处理函数
	targetNode.node.HandleFunc(ctx)
}
```

### 2.2 运行测试用例

此处运行2.1.4小节的测试用例即可

## PART3. 将参数名/值传递至业务处理函数

到目前为止,我们只是拿到了参数路径中的名值对,还没有实现让业务处理函数获取到这个名值对的功能.

### 3.1 修改`Context`

`context.go`:

```go
package valueOfParamRoute

import "net/http"

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
}
```

### 3.2 命中节点后将名值对传递给`Context`

`httpServer.go`:

```go
// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context) {
	method := ctx.Req.Method
	path := ctx.Req.URL.Path
	targetNode, ok := s.findRoute(method, path)
	// 没有在路由树中找到对应的路由节点 或 找到了路由节点的处理函数为空(即NPE:none pointer exception 的问题)
	// 则返回404
	if !ok || targetNode.node.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		// 此处确实会报错 但是作为一个WEB框架 遇上了这种错误也没有特别好的处理办法
		// 最多只能是落个日志
		_, _ = ctx.Resp.Write([]byte("Not Found"))
		return
	}
	
	// 命中节点则将路径参数名值对设置到上下文中
	ctx.PathParams = targetNode.pathParams

	// 执行路由节点的处理函数
	targetNode.node.HandleFunc(ctx)
}
```

## PART4. 参数路径的冲突问题

### 4.1 问题的产生

思考这样一个问题:我们是否允许形如`/user/:id`和`/user/:name`同时存在?

答案显而易见:肯定是不支持两个路由同时存在的.

### 4.2 修改`childOrCreate()`方法

`node.go`:

```go
// childOrCreate 本方法用于在节点上获取给定的子节点,如果给定的子节点不存在则创建
func (n *node) childOrCreate(segment string) *node {
	// 如果路径为参数 则查找当前节点的参数子节点 或创建一个当前节点的参数子节点 并返回
	if strings.HasPrefix(segment, ":") {
		// 若当前节点存在通配符子节点 则不允许注册参数子节点
		if n.wildcardChild != nil {
			panic("web: 非法路由,已有通配符路由.不允许同时注册通配符路由和参数路由")
		}

		// 若当前节点的参数子节点不为空 说明当前节点已被注册了一个参数子节点 不允许再注册参数子节点
		if n.paramChild != nil {
			msg := fmt.Sprintf("web: 路由冲突,参数路由冲突.已存在路由 %s", n.paramChild.path)
			panic(msg)
		}

		n.paramChild = &node{
			path: segment,
		}
		return n.paramChild
	}

	// 若路径为通配符 则查找当前节点的通配符子节点 或创建一个当前节点的通配符子节点 并返回
	if segment == "*" {
		// 若当前节点存在参数子节点 则不允许注册通配符子节点
		if n.paramChild != nil {
			panic("web: 非法路由,已有参数路由.不允许同时注册通配符路由和参数路由")
		}

		if n.wildcardChild == nil {
			n.wildcardChild = &node{
				path: segment,
			}
		}
		return n.wildcardChild
	}

	// 如果当前节点的子节点映射为空 则创建一个子节点映射
	if n.children == nil {
		n.children = map[string]*node{}
	}

	res, ok := n.children[segment]
	// 如果没有找到子节点,则创建一个子节点;否则返回找到的子节点
	if !ok {
		res = &node{
			path: segment,
		}
		n.children[segment] = res
	}
	return res
}
```

### 4.3 测试

`router_test.go`:

```go
// TestRouter_findRoute_same_param_coexist 测试针对参数路由时,已有同名参数路由的情况
func TestRouter_findRoute_same_param_coexist(t *testing.T) {
	// step1. 注册有冲突的路由
	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	r.addRoute(http.MethodGet, "/order/detail/:id", mockHandleFunc)

	// step2. 断言非法用例
	assert.Panicsf(t, func() {
		r.addRoute(http.MethodGet, "/order/detail/:name", mockHandleFunc)
	}, "web: 路由冲突,参数路由冲突.已存在路由 id")
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://go.sai.show/part03.-lu-you-shu/3.11-lu-you-shu-can-shu-lu-jing-zhi-can-shu-zhi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
