# README

## 说明

**Github地址(欢迎star):** [**https://github.com/step-by-step-wiki/GoBook**](https://github.com/step-by-step-wiki/GoBook)

**Gitbook在线版:** [**为你自己学Go**](https://go.sai.show)

**另外还有一本Gitbook, 是Go的学习,** [**为你自己学Redis**](https://redis.sai.show)**, 欢迎学习和star🌟**

## About me

**Url**: [**https://sai.show/about**](https://sai.show/about)

## Highlights

目前在跟邓明老师学习，每次学习完，都会进行大量练习，然后把老师讲的，和自己整理的（包含自己的截图、自己画的流程截图）写下来，因为自己也在转Go语言开发，把Go做为职业生涯的第二语言，我想着通过一个学习者的方式去形成一本书，这本书为你自己而学，为自己多一条选择而学，所以书名命为【为你自己学Go】，希望能对学习Go的大家有所帮助～

在做一件有意义的事情，文章会一直更新，直到结束

![Web框架概览](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d95d63d4b4097728adb4c67da4357626c76ef531%2FWeb%E6%A1%86%E6%9E%B6%E6%A6%82%E8%A7%88.png?alt=media)

代码在[code](https://github.com/step-by-step-wiki/GoBook/blob/main/code/README.md)目录下

文章按章节如下：

* Web框架之 Server与路由树
  * PART01.Web框架概览
    * [01.Web框架概览-学习路线](/part01.web-kuang-jia-gai-lan/1.01-web-kuang-jia-gai-lan-xue-xi-lu-xian)
    * [02.Web框架概览-Beego框架分析](/part01.web-kuang-jia-gai-lan/1.02-web-kuang-jia-gai-lan-beego-kuang-jia-fen-xi)
    * [03.Web框架概览-GIN框架分析](/part01.web-kuang-jia-gai-lan/1.03-web-kuang-jia-gai-lan-gin-kuang-jia-fen-xi)
    * [04.Web框架概览-Iris框架分析](/part01.web-kuang-jia-gai-lan/1.04-web-kuang-jia-gai-lan-iris-kuang-jia-fen-xi)
    * [05.Web框架概览-Echo框架分析](/part01.web-kuang-jia-gai-lan/1.05-web-kuang-jia-gai-lan-echo-kuang-jia-fen-xi)
  * PART02.Server
    * [01.Server详解与面试要点](/part02.server/2.01-server-xiang-jie-yu-mian-shi-yao-dian)
  * PART03.路由树
    * [01.路由树-Beego\&GIN\&Echo实现与设计总结](/part03.-lu-you-shu/3.01-lu-you-shu-beegoginecho-shi-xian-yu-she-ji-zong-jie)
    * [02.路由树-全静态匹配](/part03.-lu-you-shu/3.02-lu-you-shu-quan-jing-tai-pi-pei)
    * [03.路由树-TDD起步](/part03.-lu-you-shu/3.03-lu-you-shu-tdd-qi-bu)
    * [04.路由树-静态匹配测试用例](/part03.-lu-you-shu/3.04-lu-you-shu-jing-tai-pi-pei-ce-shi-yong-li)
    * [05.路由树-静态匹配之路由查找](/part03.-lu-you-shu/3.05-lu-you-shu-jing-tai-pi-pei-zhi-lu-you-cha-zhao)
    * [06.路由树-静态匹配之集成Server](/part03.-lu-you-shu/3.06-lu-you-shu-jing-tai-pi-pei-zhi-ji-cheng-server)
    * [07.路由树-通配符匹配之路由注册](/part03.-lu-you-shu/3.07-lu-you-shu-tong-pei-fu-pi-pei-zhi-lu-you-zhu-ce)
    * [08.路由树-通配符匹配之路由查找与测试](/part03.-lu-you-shu/3.08-lu-you-shu-tong-pei-fu-pi-pei-zhi-lu-you-cha-zhao-yu-ce-shi)
    * [09.路由树-参数路径之基本注册和查找](/part03.-lu-you-shu/3.09-lu-you-shu-can-shu-lu-jing-zhi-ji-ben-zhu-ce-he-cha-zhao)
    * [10.路由树-参数路径之校验](/part03.-lu-you-shu/3.10-lu-you-shu-can-shu-lu-jing-zhi-jiao-yan)
    * [11.路由树-参数路径之参数值](/part03.-lu-you-shu/3.11-lu-you-shu-can-shu-lu-jing-zhi-can-shu-zhi)
    * [12.路由树-总结与面试要点](/part03.-lu-you-shu/3.12-lu-you-shu-zong-jie-yu-mian-shi-yao-dian)
  * PART04.课后复习
    * [01.课后复习-Server](/part04.-ke-hou-fu-xi/4.01-ke-hou-fu-xi-server)
    * [02.课后复习-Route](/part04.-ke-hou-fu-xi/4.02-ke-hou-fu-xi-route)
* Web框架之Context与AOP方案
  * PART05.Context
    * [01.Context-简介](/part05.context/5.01-context-jian-jie)
    * [02.Context-Beego-Context设计分析](/part05.context/5.02-contextbeegocontext-she-ji-fen-xi)
    * [03.Context-Gin-Context设计分析](/part05.context/5.03-contextgincontext-she-ji-fen-xi)
    * [04.Context-Echo和Iris的Context设计分析](/part05.context/5.04-contextecho-he-iris-de-context-she-ji-fen-xi)
    * [05.Context-处理输入输出总结](/part05.context/5.05-context-chu-li-shu-ru-shu-chu-zong-jie)
    * [06.Context-处理输入之Body输入](/part05.context/5.06-context-chu-li-shu-ru-zhi-body-shu-ru)
    * [07.Context-处理输入之表单输入](/part05.context/5.07-context-chu-li-shu-ru-zhi-biao-dan-shu-ru)
    * [08.Context-处理输入之查询参数、路径参数和StringValue](/part05.context/5.08-context-chu-li-shu-ru-zhi-cha-xun-can-shu-lu-jing-can-shu-he-stringvalue)
    * [09.Context-处理输出](/part05.context/5.09-context-chu-li-shu-chu)
    * [10.Context-总结与面试要点](/part05.context/5.10-context-zong-jie-yu-mian-shi-yao-dian)
  * PART06.AOP
    * [01.AOP简介与不同框架设计概览](/part06.aop/6.01-aop-jian-jie-yu-bu-tong-kuang-jia-she-ji-gai-lan)
    * [02.AOP设计方案-Middleware](/part06.aop/6.02-aop-she-ji-fang-an-middleware)
  * PART07.Middleware
    * [01.Middleware-AccessLog](/part07.middleware/7.01-middleware-accesslog)
    * [02.Middleware-Trace简介和OpenTelemetry](/part07.middleware/7.02-middlewaretrace-jian-jie-he-opentelemetry)
    * [03.Middleware-OpenTelemetry测试](/part07.middleware/7.03-middlewareopentelemetry-ce-shi)
    * [04.Middleware-OpenTelemetry总结](/part07.middleware/7.04-middlewareopentelemetry-zong-jie)
    * [05.Prometheus详解](/part07.middleware/7.05-prometheus-xiang-jie)
    * [06.Middleware-Prometheus](/part07.middleware/7.06-middleware-prometheus)
    * [07.Middleware-错误页面](/part07.middleware/7.07-middleware-cuo-wu-ye-mian)
    * [08.Middleware-从panic中恢复](/part07.middleware/7.08-middleware-cong-panic-zhong-hui-fu)
    * [09.Middleware总结和面试](/part07.middleware/7.09-middleware-zong-jie-he-mian-shi)
  * PART08.Review
    * [01.课后复习-AOP](/part08.review/8.01-ke-hou-fu-xi-aop)
    * [02.课后复习-Context](/part08.review/8.02-ke-hou-fu-xi-context)
    * [03.课后复习-Middleware-AccessLog](/part08.review/8.03-ke-hou-fu-xi-middlewareaccesslog)
  * PART09.Appendix
    * [01附录.责任链模式](/part09.appendix/01-fu-lu-.-ze-ren-lian-mo-shi)
    * [02附录.生成器模式](/part09.appendix/02-fu-lu-.-sheng-cheng-qi-mo-shi)
    * [03附录.函数选项模式](/part09.appendix/03-fu-lu-.-han-shu-xuan-xiang-mo-shi)
  * 好用的小程序(欢迎体验呀)
    * [01.去抖音水印](/xiaochengxu/01.qushuiyin)
    * [02.密码管理](/xiaochengxu/02.password)

## 如果可以，请关注我的公众号

绝不打扰，如果您关注我了，后期等我运营好了，一定给关注我的你们发福利😊

![公众号](https://oss.laf.run/v8w6wa-keepass/images/gongzhonghao.jpg)

## 赞助

如果你觉得这个项目对你有帮助，请给我点个Star。并且情况允许的话，可以给我一点点支持，总之非常感谢支持～

![微信](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-33879b15e0c80184d9eda29f027947cb3268e9ba%2Fwechat.jpg?alt=media)

WeChat Pay

![支付宝](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-dbb99af7ddb621ccc7e608bc22452d518cf83cc8%2Falipay.jpg?alt=media)

Alipay


# PART01.Web框架概览


# 1.01 Web框架概览:学习路线

![Web框架概览](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d95d63d4b4097728adb4c67da4357626c76ef531%2FWeb%E6%A1%86%E6%9E%B6%E6%A6%82%E8%A7%88.png?alt=media)

* Web框架概览

看不同的框架是怎么设计的

* 路由匹配

  课上会讲3种:

  * 静态路由匹配
  * 通配符匹配
  * 参数路径

  作业:

  * 正则匹配
* AOP方案

  通过AOP方案实现可观测性Middleware(tracing、metrics、log)

以上是一个Web框架的核心部分.后续的页面渲染、文件处理与静态资源、Session都可以认为是为了方便使用者而额外引入的功能.因此在进行这3部分的开发时,不会再侵入核心代码.当然,也可以做成侵入式的.


# 1.02 Web框架概览-Beego框架分析

参考其它Web框架来设计自己的框架.

## PART1. 基本使用

Beego是基于MVC(Model-View-Controller)的,所以它定义了一个核心接口`ControllerInterface`.`ControllerInterface`定义了一个控制器**必须要解决什么问题**.

示例:

定义Controller:

```GO
package beego

import "github.com/beego/beego/v2/server/web"

type UserController struct {
	web.Controller
}

func (c *UserController) GetUser() {
	c.Ctx.WriteString("你好，我是大明")
}

func (c *UserController) CreateUser() {
	u := &User{}
	err := c.Ctx.BindJSON(u)
	if err != nil {
		c.Ctx.WriteString(err.Error())
		return
	}

	_ = c.Ctx.JSONResp(u)
}

type User struct {
	Name string
}
```

可以看到,自定义的Controller是组合了Beego的`web.Controller`的.

使用Controller:

```GO
package beego

import (
	"github.com/beego/beego/v2/server/web"
	"testing"
)

func TestUserController(t *testing.T) {
	web.BConfig.CopyRequestBody = true
	c := &UserController{}
	web.Router("/user", c, "get:GetUser")
	// 监听 8081 端口
	web.Run(":8081")

}
```

注意`web.Router("/user", c, "get:GetUser")`,将uri`/user`映射到了Controller`UserController`的`GetUser()`方法上.`"get:GetUser"`中的`get`要求HTTP动词为GET.

## PART2. 核心抽象

那么问题来了,Beego的核心抽象是什么?

[ControllerInterface](https://github.com/beego/beego/blob/develop/server/web/controller.go#L137)

```GO
// ControllerInterface is an interface to uniform all controller handler.
type ControllerInterface interface {
	Init(ct *context.Context, controllerName, actionName string, app interface{})
	Prepare()
	Get()
	Post()
	Delete()
	Put()
	Head()
	Patch()
	Options()
	Trace()
	Finish()
	Render() error
	XSRFToken() string
	CheckXSRFCookie() bool
	HandlerFunc(fn string) bool
	URLMapping()
}
```

* `Get()`、`Post()`、`Delete()`、`Put()`、`Head()`、`Patch()`、`Options()`定义了如何处理HTTP动词
* `Init()`、`Prepare()`、`Finish()`定义了一个Controller的生命周期.`Prepare()`类似于一个BeforeRequest的过程,`Finish()`类似于一个AfterRequest的过程,本质上二者都是回调
* 实际上刚刚示例中的`web.Contrller`就是接口`ControllerInterface`的默认实现.自定义的Controller只需组合`web.Contrller`即可

注意示例中的如下2行代码:

```GO
web.Router("/user", c, "get:GetUser")
web.Run(":8081")
```

这说明用户虽然被要求组合`web.Contrller`,但路由注册和服务器启动是通过另一套机制来完成的.换言之,**Beego框架中的路由注册与Controller的组合是无关的**.Controller仅仅用于帮助组织业务代码.

## PART3. HttpServer和ControllerRegister

`ControllerInterface`可以看做核心接口,因为它直接体现了Beego的设计初衷:MVC模式.同时它也是用户核心接入点.

但是如果从功能特性上来说,[HttpServer](https://github.com/beego/beego/blob/develop/server/web/server.go#L50)和[ControllerRegister](https://github.com/beego/beego/blob/develop/server/web/router.go#L160)才是核心

* `HttpServer`:代表一个"服务器",大多数时候它就是一个进程.
  * 当然你也可以做成1个进程监听多个端口的形式,但是端口与端口之间是资源隔离的
* `ControllerRegister`:注册路由、路由匹配、执行业务代码都是通过它来完成的

## PART4. Context抽象

和之前的内容相比,其他抽象更侧重于用户友好性.[Context](https://github.com/beego/beego/blob/develop/server/web/context/context.go#L72)抽象代表的是整个请求执行过程的上下文,用户操作请求和响应是通过Ctx来达成的.

实际上在Beego默认实现的Controller中也有这个[Ctx](https://github.com/beego/beego/blob/develop/server/web/controller.go#L109).一般情况下这个`Ctx`代表了整个请求过程的上下文.

```GO
// Context Http request context struct including BeegoInput, BeegoOutput, http.Request and http.ResponseWriter.
// BeegoInput and BeegoOutput provides an api to operate request and response more easily.
type Context struct {
	Input          *BeegoInput
	Output         *BeegoOutput
	Request        *http.Request
	ResponseWriter *Response
	_xsrfToken     string
}
```

Beego对Context进行了一个细分,分为`Input`和`Output`.`Request`字段是请求的副本,而`ResponseWriter`字段则是对响应的一个封装.

也有人认为`Input`应该包含`Request`,`Output`应该包含`ResponseWriter`,这样的设计会更清晰.

## PART5. 核心抽象总结

![Beego核心抽象](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-ac63fbc95bc965014adef7deb7fd457ed31cd814%2FBeego%E6%A0%B8%E5%BF%83%E6%8A%BD%E8%B1%A1.png?alt=media)

* `ControllerRegister`:最为基础,它解决了**路由注册**和**路由匹配**这个基础问题
* `Context`和`Controller`:为用户提供了丰富API,用于辅助构建系统
  * 当然,硬要说的话,没有这二者也不是不行.只是用户需要手动和`http.Request`和`http.ResponseWriter`交互,这二者也只是对`http.Request`和`http.ResponseWriter`的封装
* `HttpServer`:作为服务器抽象,用于管理应用生命周期和资源隔离单位

## 附录:1个进程监听2个端口的方法

```GO
package beego

import (
	"github.com/beego/beego/v2/server/web"
	"testing"
)

func TestUserController(t *testing.T) {
	go func() {
		s := web.NewHttpSever()
		s.Run(":8082")
	}()
	web.BConfig.CopyRequestBody = true
	c := &UserController{}
	web.Router("/user", c, "get:GetUser")
	// 监听 8081 端口
	web.Run(":8081")
}
```

此时访问`localhost:8082/user`是无法得到响应的.只有访问`localhost:8081/user`能够得到响应.

1个进程监听2个端口的使用场景:实时更新配置.例如同一个进程内,端口8081可以修改8082的内存


# 1.03 Web框架概览-GIN框架分析

## PART1. 基本使用

定义Controller:

```GO
package gin

import "github.com/gin-gonic/gin"

type UserController struct {
}

func (c *UserController) GetUser(ctx *gin.Context) {
	ctx.String(200, "hello, world")
}
```

**注意:此时我们自定义的Controller并不像使用Beego时,组合了框架内置的Controller的**

使用Controller:

```GO
package gin

import (
	"github.com/gin-gonic/gin"
	"net/http"
	"testing"
)

func TestUserController_GetUser(t *testing.T) {
	g := gin.Default()
	ctrl := &UserController{}
	g.GET("/user", ctrl.GetUser)
	g.POST("/user", func(ctx *gin.Context) {
		ctx.String(http.StatusOK, "hello %s", "world")
	})

	g.GET("/static", func(context *gin.Context) {
		// 读文件
		// 写响应
	})
	_ = g.Run(":8082")
}
```

注意:此处注册路由使用了2种方式.

方式1:直接将Controller上的方法注册到Server上,例如`g.GET("/user", ctrl.GetUser)`

方式2:将一个匿名函数注册到Server上,例如:

```GO
g.POST("/user", func(ctx *gin.Context) {
	ctx.String(http.StatusOK, "hello %s", "world")
})
```

从使用上就可以看出,GIN没有像Beego一样对使用者做出一个基本MVC模型的假设(或者可以说是要求我认为).

## PART2. IRoutes接口

[IRoutes](https://github.com/gin-gonic/gin/blob/master/routergroup.go#L33)接口是GIN的核心接口,提供了注册路由的抽象.提及到这些功能,那么GIN的IRoutes接口就很像Beego的[ControllerRegister](https://github.com/beego/beego/blob/develop/server/web/router.go#L160)接口了.

实际上GIN的[`Engine`](https://github.com/gin-gonic/gin/blob/master/gin.go#L84)结构体就是组合了该接口的实现.

```GO
// IRoutes defines all router handle interface.
type IRoutes interface {
	Use(...HandlerFunc) IRoutes

	Handle(string, string, ...HandlerFunc) IRoutes
	Any(string, ...HandlerFunc) IRoutes
	GET(string, ...HandlerFunc) IRoutes
	POST(string, ...HandlerFunc) IRoutes
	DELETE(string, ...HandlerFunc) IRoutes
	PATCH(string, ...HandlerFunc) IRoutes
	PUT(string, ...HandlerFunc) IRoutes
	OPTIONS(string, ...HandlerFunc) IRoutes
	HEAD(string, ...HandlerFunc) IRoutes
	Match([]string, string, ...HandlerFunc) IRoutes

	StaticFile(string, string) IRoutes
	StaticFileFS(string, string, http.FileSystem) IRoutes
	Static(string, string) IRoutes
	StaticFS(string, http.FileSystem) IRoutes
}
```

* `Use()`方法:提供了用户**接入自定义逻辑的能力**,通常也被看做是插件机制.和后边要讲的AOP方案本质上是相同的
* 处理静态文件相关的行为
  * `StaticFile()`方法
  * `StaticFileFS()`方法
  * `Static()`方法
  * `StaticFS()`方法
* `Handle()`方法:处理注册路由的行为

此时我们先不管其他几个HTTP动词的方法,先来看一下我们注册一个路由时(例如上文例子中的`g.GET()`/`g.POST()`)的调用链:

`Engine.GET()`源码:

```GO
// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}
```

`Engine.POST()`源码:

```GO
// POST is a shortcut for router.Handle("POST", path, handle).
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodPost, relativePath, handlers)
}
```

很明显它们调用的都是`group.handle()`方法.我们再来看一下RouterGroup(实际上也是IRoutes的实现)的`Handle()`方法:

```GO
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	if matched := regEnLetter.MatchString(httpMethod); !matched {
		panic("http method " + httpMethod + " is not valid")
	}
	return group.handle(httpMethod, relativePath, handlers)
}
```

破案了,结论:接口`IRoutes`中的HTTP动词相关的方法(例如`GET()`/`POST()`等)本质上和调用其`Handle()`方法的底层是完全相同的,都是调用的`RouterGroup.handle()`方法.

一种观点认为:`IRoutes`接口作为核心接口,是没有必要在其定义中保留HTTP动词相关的方法,仅有`Handle()`方法即可.

抱持上述观点的人也同样认为处理静态文件相关的方法也不需要定义在核心接口`IRoutes`中.可以像上述例子中的如下代码:

```GO
g.GET("/static", func(context *gin.Context) {
	// 读文件
	// 写响应
})
```

的做法,框架只需提供具有"读文件"和"写响应"的方法,让使用者自行将该方法注册到路由上即可.没有必要将处理静态文件相关的方法定义在核心接口`IRoutes`中.

我个人认为:HTTP动词相关的方法被定义出来,可能更多的是为了提升使用者写出的代码可读性.所以其定义确实不必放在核心接口中.

再次强调:GIN是没有设计Controller的抽象的.这方面讲师和GIN框架设计者的意见比较一致,他们认为:**是否使用MVC模式是交由使用者决定的,而非一个中间件设计者需要考虑的**.

Beego的设计限制(或者可以说强制要求)使用者组合其内置的Controller,这样的设计会侵入框架使用者的代码.而GIN的设计则将"是否使用MVC模式"的决定权交由使用者决定.

由此引申出的一个问题是:\*\*MVC模式究竟是一个WEB框架本身就应该具有的形式,还是使用者组织路由的形式之一?\*\*讲师抱持的是后者的观点,即:**MVC是使用者组织路由的形式之一**,使用者同样可以像上述例子中,使用匿名函数来完成路由注册.

## PART3. Engine的实现

### 3.1 基本概念

[Engine](https://github.com/gin-gonic/gin/blob/master/gin.go#L84)可以看做是Beego中HttpServer和ControllerRegister的结合体.

```GO
// Engine is the framework's instance, it contains the muxer, middleware and configuration settings.
// Create an instance of Engine, by using New() or Default()
type Engine struct {
	RouterGroup

	RedirectTrailingSlash bool

	RedirectFixedPath bool

	HandleMethodNotAllowed bool

	ForwardedByClientIP bool

	AppEngine bool

	UseRawPath bool

	UnescapePathValues bool

	RemoveExtraSlash bool

	RemoteIPHeaders []string

	TrustedPlatform string

	MaxMultipartMemory int64

	UseH2C bool

	ContextWithFallback bool

	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
	trustedCIDRs     []*net.IPNet
}
```

上文已经说过,结构体`RouterGroup`是接口`IRoutes`的实现.可以看到`Engine`组合了`RouterGroup`,自然`Engine`也是接口`IRoutes`的实现.因此其具有Beego中ControllerRegister的部分功能.

### 3.2 Engine是接口`http.Handler`的实现

`Engine`本身可以作为一个Handler传递到http包,用于启动服务器,示例如下:

```GO
package gin

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func UseEngineAsHandler() {
	g := gin.Default()
	http.ListenAndServe(":", g)
}
```

这是因为`Engine`实现了接口`http.Handler`:

```GO
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}
```

### 3.3 Engine中的`tree`字段--`methodTrees`和`methodTree`

`Engine`的路由树功能本质上是依赖于[`methodTree`](https://github.com/gin-gonic/gin/blob/master/gin.go#L166)的.其定义如下:

```GO
type methodTrees []methodTree
```

1个HTTP动词有1棵树,这些树构成了一片"森林",即`methodTrees`.而`methodTree`才是真实的路由树:

```GO
type methodTree struct {
	method string
	root   *node
}
```

### 3.4 HandlerFunc和HandlersChain

`HandlerFunc`定义了核心抽象——处理逻辑.在默认情况下,它代表了注册路由的业务代码.

```GO
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
```

简单理解`HandlerFunc`就是你的业务代码.在第一部分的例子中,匿名函数和`ctrl.GetUser`其实就是`HandlerFunc`

可以将多个`HandlerFunc`整理成1个`HandlersChain`,构造成责任链模式:

```GO
// HandlersChain defines a HandlerFunc slice.
type HandlersChain []HandlerFunc
```

![责任链模式](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-ff6ed31e45e5bfa323df29f0c32f9593f2fe472b%2F%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F.png?alt=media)

上图中的`HandlerFunc1`、`HandlerFunc2`、`HandlerFunc3`是一些其他操作(我个人理解类似Middleware),你写的业务逻辑是`HandlerFunc4`,放在了最后执行.

核心:当路由匹配命中之后,并非直接命中到你的业务逻辑代码,而是匹配到了一条责任链,在这条责任链上,先执行一些中间件,最后才执行你的业务逻辑代码.

TODO:问题:如果我想在我的业务逻辑代码之后再执行某些操作(例如记录我的业务逻辑代码执行的时长),是不是这个模式就不适合了?

## PART4. Context抽象

[Context](https://github.com/gin-gonic/gin/blob/master/context.go#L51)也是代表了执行的上下文,提供了丰富的API:

```GO
// Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example.
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine
	params       *Params
	skippedNodes *[]skippedNode

	// This mutex protects Keys map.
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]any

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache caches the query result from c.Request.URL.Query().
	queryCache url.Values

	// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	sameSite http.SameSite
}
```

* 处理请求的API:代表的是以Get和Bind为前缀的方法
* 处理响应的API:例如返回JSON或者XML响应的方法
* 渲染页面的API:如HTML方法

![责任链模式](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-436dbe8a6c790810ff98f2788b044a80d8ba3486%2FContext%E6%8A%BD%E8%B1%A1.png?alt=media)

**这个结构体的API要多看!**

## PART5. 核心抽象总结

![核心抽象总结](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-c2a7f4af465b3a8bbe8591243022be3677f6f379%2F%E6%A0%B8%E5%BF%83%E6%8A%BD%E8%B1%A1%E6%80%BB%E7%BB%93.png?alt=media)

* `methodTree`&`node`:表示了一棵路由树
* `Engine`:Beego中HttpServer和ControllerRegister的结合体,完成了路由匹配和服务启动的功能
* `Context`:和Beego一样作为每个请求的上下文使用
* `HandlerFunc`:Beego的`ControllerInterface`中也有一个`HandlerFunc()`方法


# 1.04 Web框架概览-Iris框架分析

## PART1. 基本使用

```GO
package iris

import (
	"github.com/kataras/iris/v12"
	"testing"
)

func TestHelloWorld(t *testing.T) {
	app := iris.New()

	app.Get("/", func(ctx iris.Context) {
		_, _ = ctx.HTML("Hello <strong>%s</strong>!", "World")
	})

	_ = app.Listen(":8083")
}
```

和GIN相同,注册路由时可以使用匿名函数,也可以使用handleFunc.

## PART2. Application

上述示例中的`iris.New()`,实际上返回的是一个`Application`实例:

```GO
// Application is responsible to manage the state of the application.
// It contains and handles all the necessary parts to create a fast web server.
type Application struct {
	// routing embedded | exposing APIBuilder's and Router's public API.
	*router.APIBuilder
	*router.Router
	ContextPool *context.Pool

	config *Configuration

	logger *golog.Logger

	I18n *i18n.I18n

	// view engine
	view view.View
	// used for build
	builded     bool
	defaultMode bool

	mu sync.Mutex
	Hosts             []*host.Supervisor
	hostConfigurators []host.Configurator
}
```

这个`Application`就是Iris中的核心抽象,它代表的是"应用".从语义上来讲`Application`更接近Beego中的`HttpServer`和GIN中的`Engine`.这一点从例子中的`app.Listen(":8083")`这行代码就能看出来.它提供了:

* 生命周期控制功能:例如`Application.Run()`/`Application.Shutdown()`等方法
* 注册路由的API:例如`Application`中的`APIBuilder`字段(该字段的类型为`*router.APIBuilder`),该字段有`APIBuilder.Get()`/`APIBuilder.Post()`等方法

一种观点认为:Application这个名字不是很合适,**因为有一些应用会监听多个端口,不同的端口提供不同的功能**(这个之前的笔记里有写到),也就是常说的多Server应用.相比之下,HttpServer或Engine更合适一些.

## PART3. 路由相关功能

Iris的设计非常复杂.在Beego和Gin里面能够明显看到路由树的痕迹(Beego中的ControllerRegister和GIN中的methodTrees),但是在Iris里面就很难看出来.

Iris中和处理路由相关的有3个抽象:Route、APIBuilder、repository

### 3.1 Route

Route:直接代表了已经注册的路由.在Beego和GIN中,对应的是路由树中的节点(例如GIN中的`gin.node`)

```GO
// Route contains the information about a registered Route.
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
	// The Party which this Route was created and registered on.
	Party       Party
	Title       string         `json:"title"`       // custom name to replace the method on debug logging.
	Name        string         `json:"name"`        // "userRoute"
	Description string         `json:"description"` // "lists a user"
	Method      string         `json:"method"`      // "GET"
	StatusCode  int            `json:"statusCode"`  // 404 (only for HTTP error handlers).
	methodBckp  string         // if Method changed to something else (which is possible at runtime as well, via RefreshRouter) then this field will be filled with the old one.
	Subdomain   string         `json:"subdomain"` // "admin."
	tmpl        macro.Template // Tmpl().Src: "/api/user/{id:uint64}"

	beginHandlers context.Handlers
	builtinBeginHandlers context.Handlers

	Handlers         context.Handlers `json:"-"`
	MainHandlerName  string           `json:"mainHandlerName"`
	MainHandlerIndex int              `json:"mainHandlerIndex"`
	doneHandlers context.Handlers

	Path string `json:"path"` // the underline router's representation, i.e "/api/user/:id"
	FormattedPath string `json:"formattedPath"`

	SourceFileName   string `json:"sourceFileName"`
	SourceLineNumber int    `json:"sourceLineNumber"`

	RegisterFileName   string `json:"registerFileName"`
	RegisterLineNumber int    `json:"registerLineNumber"`

	topLink *Route
	overlappedLink *Route

	// Sitemap properties: https://www.sitemaps.org/protocol.html
	NoSitemap  bool      // when this route should be hidden from sitemap.
	LastMod    time.Time `json:"lastMod,omitempty"`
	ChangeFreq string    `json:"changeFreq,omitempty"`
	Priority   float32   `json:"priority,omitempty"`

	// ReadOnly is the read-only structure of the Route.
	ReadOnly context.RouteReadOnly

	// OnBuild runs right before BuildHandlers.
	OnBuild func(r *Route)
	NoLog   bool // disables debug logging.
}
```

### 3.2 APIBuilder

APIBuilder:创建Route的Builder模式,Party也是它创建的.

```GO
// APIBuilder the visible API for constructing the router
// and child routers.
type APIBuilder struct {
	logger *golog.Logger
	parent *APIBuilder // currently it's not used anywhere.

	apiBuilderDI *APIContainer

	macros *macro.Macros
	properties context.Map
	routes *repository
	routesNoLog bool

	middleware          context.Handlers
	middlewareErrorCode context.Handlers
	beginGlobalHandlers context.Handlers

	// the per-party done handlers, order matters.
	doneHandlers context.Handlers
	doneGlobalHandlers context.Handlers

	relativePath string
	allowMethods []string

	handlerExecutionRules ExecutionRules
	routeRegisterRule RouteRegisterRule

	routerFilterHandlers context.Handlers
	routerFilters map[Party]*Filter
	partyMatcher PartyMatcherFunc
}
```

### 3.3 repository

repository:存储了所有的Routes,有点接近GIN的methodTrees的概念

```GO
// repository passed to all parties(subrouters), it's the object witch keeps
// all the routes.
type repository struct {
	routes []*Route
	paths  map[string]*Route // only the fullname path part, required at CreateRoutes for registering index page.
}
```

讲师观点:过于复杂,职责不清晰,难以划定每个结构体的职责边界.不符合一般人的直觉,新人学习和维护门槛高,不要学.

## PART4. Context抽象

Context也是表示一个请求的**上下文**.它本身也提供了**各种处理请求和响应的方法**.基本上和Beego、GIN的Context没啥区别.比较有特色的一点是:**Iris的Context支持请求级别的添加Handler,即`AddHandler()`方法**.有点像GIN中IRoutes接口中定义的`Handle()`方法,可以看到二者AOP的粒度是不同的,Iris是在方法级别上控制,而GIN是在Server级别控制.

## PART5. 抽象总结

![核心抽象总结](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-7aa4a6eaa8e96679446209a2edd408987fa64b64%2F%E6%A0%B8%E5%BF%83%E6%8A%BD%E8%B1%A1%E6%80%BB%E7%BB%93.png?alt=media)

* Route & Party & APIBuilder:解决路由相关(路由注册/路由匹配等)的问题
* Application:解决HTTPServer的问题
* Context:解决请求的上下文问题
* Handler:具体的业务逻辑代码


# 1.05 Web框架概览-Echo框架分析

## PART1. 基本使用

```GO
package echo

import (
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"net/http"
	"testing"
)

func TestHelloWorld(t *testing.T) {
	// Echo instance
	e := echo.New()

	// Middleware
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())

	// Routes
	e.GET("/", hello)

	// Start server
	e.Logger.Fatal(e.Start(":8084"))
}

// Handle
func hello(c echo.Context) error {
	return c.String(http.StatusOK, "Hello, World!")
}
```

## PART2. 结构体`echo.Echo`

从上述示例的`e.Start(":8084")`可以看出,`echo.Echo`结构体(即`echo.New()`的返回值)类似于Beego的HttpServer和GIN的Engine:

* 注册路由:暴露了注册路由的方法,但`echo.Echo`本身并不是路由树的载体(`echo.Echo`只是组合了`echo.Router`)
* 生命周期管理:如`Echo.Start()`/`Echo.Shutdown()`等方法

```GO
// Echo is the top-level framework instance.
Echo struct {
	filesystem
	common
	startupMutex     sync.RWMutex
	StdLogger        *stdLog.Logger
	colorer          *color.Color
	premiddleware    []MiddlewareFunc
	middleware       []MiddlewareFunc
	maxParam         *int
	router           *Router
	routers          map[string]*Router
	pool             sync.Pool
	Server           *http.Server
	TLSServer        *http.Server
	Listener         net.Listener
	TLSListener      net.Listener
	AutoTLSManager   autocert.Manager
	DisableHTTP2     bool
	Debug            bool
	HideBanner       bool
	HidePort         bool
	HTTPErrorHandler HTTPErrorHandler
	Binder           Binder
	JSONSerializer   JSONSerializer
	Validator        Validator
	Renderer         Renderer
	Logger           Logger
	IPExtractor      IPExtractor
	ListenerNetwork  string
}
```

可以看到在该结构体中,有2个相似的字段:`router`和`routers`:

* `router`:表示路由树
* `routers`:表示根据Host将路由树隔离,可以看做是类似namespace之类的概念,**既是一种组织方式,也是一种隔离机制**

一种观点认为:如果想要做一个类似namespace之类的隔离机制,应该使用一些更轻量级的方式.但如果仅仅是为了实现"根据Host将路由树隔离"的功能,不如创建2个`Echo`实例,而不是使用在1个`Echo`示例中使用一个map通过key来隔离.

## PART3. Route和node

Router:表示路由树,而node表示的是路由树上的节点

### 3.1 Router

```GO
Router struct {
	tree   *node
	routes map[string]*Route
	echo   *Echo
}
```

#### 3.1.1 Route类型

实际上这里的`Route`类型才是真正表示1个路由的:

```GO
// Route contains a handler and information for matching against requests.
Route struct {
	Method string `json:"method"`
	Path   string `json:"path"`
	Name   string `json:"name"`
}
```

`Router.tree`字段表示一棵树的根节点,实际上该字段改名为`root`更加合适.

#### 3.1.2 Router.echo

从上述`echo.Echo`结构体的定义中可以看到它有一个字段名为`router`,其类型为`*Router`;从`Router`结构体的定义中也能够看到它有一个字段名为`echo`,其类型为`*Echo`.很明显的双向引用.

这也是在有些情况下逼不得已的做法.比如GO原生的`sql`包中,`sql.Tx`里边维护了一个`sql.DB`的实例:

```GO
type Tx struct {
	db *DB

	closemu sync.RWMutex

	dc  *driverConn
	txi driver.Tx

	releaseConn func(error)

	done int32

	keepConnOnRollback bool

	stmts struct {
		sync.Mutex
		v []*Stmt
	}

	cancel func()

	ctx context.Context
}
```

### 3.2 node

```GO
node struct {
	kind           kind
	label          byte
	prefix         string
	parent         *node
	staticChildren children
	originalPath   string
	methods        *routeMethods
	paramChild     *node
	anyChild       *node
	paramsCount    int
	// isLeaf indicates that node does not have child routes
	isLeaf bool
	// isHandler indicates that node has at least one handler registered to it
	isHandler bool

	// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
	notFoundHandler *routeMethod
}
```

注意字段`staticChildren`(实际上`children`类型就是`[]*node`)、`paramChild`和`anyChild`:

* `staticChildren`:静态匹配
* `paramChild`:参数路径
* `anyChild`:通配符匹配

**这种设计可以比较容易实现路由优先级和路由冲突检测**

## PART4. Context

Echo中的[Context](https://github.com/labstack/echo/blob/master/context.go#L19)是一个大而全的**接口**,定义了处理请求和响应的各种方法.和Beego、Gin、Iris的Context没有什么区别.

硬要说区别的话,只是在Echo中这个抽象被定义为了一个接口而非结构体.这也就意味着可以为该接口提供不同的实现.当然其实没啥人给它提供不同的实现.

## PART5. 核心抽象

![核心抽象总结](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e4e9339e55533a1e9e405189525d9a900b3c1efc%2F%E6%A0%B8%E5%BF%83%E6%8A%BD%E8%B1%A1%E6%80%BB%E7%BB%93.png?alt=media)

* Route & node:处理路由相关功能
* Echo:类似Beego中的Beego的HttpServer和GIN中的Engine
* Context:处理请求上下文
* HandlerFunc:业务逻辑代码

## PART6. 框架对比

|       |        Beego       |     GIN     |     Iris    |     Echo    |
| :---: | :----------------: | :---------: | :---------: | :---------: |
| 代表服务器 |     HttpServer     |    Engine   | Application |     Echo    |
|  代表路由 | ControllerRegister |  methodTree |    Route    |    Route    |
|  上下文  |       Context      |   Context   |   Context   |   Context   |
|  处理逻辑 |     HandlerFunc    | HandlerFunc | HandlerFunc | HandlerFunc |

所以实际上我们要造一个Web框架,就是要建立我们自己的这几个抽象.

## PART7. WEB框架面试题

实际上,面整体Web框架的还是比较少的.大多数时候,面试都是聊具体的某个Web框架.且如果你打的是一些高端局,也不会问你怎么用,最多最多是问你怎么实现.

### 7.1 Web框架拿来做什么?

处理HTTP请求,为用户提供便捷API,为用户提供无侵入式的插件机制,提供如上传下载等默认功能.

### 7.2 为什么都已经有了http包,还要开发Web框架?

* 高级路由功能
  * 参数匹配
  * 正则匹配
  * 通配符匹配
  * 这些功能shi 原生的net/http包不支持的
* 封装HTTP上下文以提供简单API
* 封装Server以提供生命周期控制
* 设计插件机制以提供无侵入式解决方案

### 7.3 Web框架的核心?

* 路由树
* 上下文Context
* Server


# PART02.Server


# 2.01 Server详解与面试要点

## PART1. 学习路线

![学习路线](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-bfcff3ac7ef0e978a4f7452db89e804c0eb0590c%2F%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF.png?alt=media)

## PART2. Web框架的核心

### 2.1 Web框架的构成

在框架对比的时候,我们注意到对于一个Web框架来说,至少要提供三个抽象:

* **代表服务器的抽象**:这里我们称之为Server
* **代表上下文的抽象**:这里我们称之为Context
* **路由树**

### 2.2 Server

从前面框架对比来看,对于一个Web框架来说,我们首先要有一个**整体代表服务器的抽象**,也就是Server.

Server从特性上来说,至少要提供三部分功能:

* **生命周期控制**:即启动、关闭.如果在后期,还要考虑增加生命周期回调特性
  * TODO:这个回调是指啥?
* **路由注册接口**:提供路由注册功能
* **作为http包到Web框架的桥梁**

如果在你的代码中,没有这个**代表服务器的抽象**(换言之,你的代码里没有定义服务器这个概念,或者说没有用于表示服务器的类),那么很多功能你无法实现.例如上边说过的生命周期控制,你的代码里都没有服务器这个"实体"(这里的实体概念上有点像CRUD中的业务实体),自然也就没有控制这个实体"成往怀灭"的能力的.

同样的,路由注册功能没有Server这个抽象也无法实现.因为不同的路由可能要注册到不同的Server上.回忆一下之前讲过的多端口进程的例子.

![Server特性](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d73b4c66fd65de7ae64abda53e1c15854c31da77%2FServer%E7%89%B9%E6%80%A7.png?alt=media)

## PART3. http.Handler接口

那么问题来了,怎么接入go原生的`net/http`包?

假设我们现在已经定义了一个Server结构体(当然我们现在还不知道它应该有什么成员属性和成员方法),那我们该如何让它和原生的`net/http`包交互?

先来看看原生的`net/http`包如何启动一个Server:

```go
package server

import (
	"net/http"
	"testing"
)

func TestServer(t *testing.T) {
	http.ListenAndServe(":8085", nil)
}
```

这样就已经启动了一个HTTP服务器.那么答案就很明确了:**用我们的Server替换掉代码中的nil,那我们的Server就和原生的`net/http`包交互了**.而我们要做的就是在请求与http包之间,做一个WEB框架.

## PART4. Server Interface的定义

### 4.1 我们需要什么?

```go
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}
```

从`http.ListenAndServe()`方法可以看出,我们需要实现一个Handler类型:

```go
type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}
```

### 4.2 构建Server Interface的方案:组合`http.Handler`

#### 4.2.1 组合`http.Handler`

通常而言,设计一个稍微复杂的系统,总是需要先使用接口来定义行为,而非直接写实现类的.

工程结构:

```
(base) root@yuanhong 01-composingHandlerIntoServer % tree ./
./
├── server.go
└── server_test.go

0 directories, 2 files
```

* `server.go`:定义Server接口

```go
package composingHandlerIntoServer

import "net/http"

type Server interface {
	// Handler 组合http.Handler接口
	http.Handler
}
```

* `server_test.go`:使用Server接口

```go
package composingHandlerIntoServer

import (
	"net/http"
	"testing"
)

func TestServer(t *testing.T) {
	var s Server
	http.ListenAndServe(":8085", s)
	http.ListenAndServeTLS(":443", "", "", s)
}
```

#### 4.2.2 组合`http.Handler`方案的优缺点

这样设计的优点:

* 用户在使用的时候只需要调用`http.ListenAndServe`就可以
* 和HTTPS协议完全无缝衔接
* 极简设计(相当于没设计)

这样设计的缺点:

* 难以控制生命周期,并且很难在控制生命周期的时候增加回调支持
  * 比如在端口监听之后,服务启动之前,想要做一些操作,这样的设计是不支持的.因为从端口启动开始,后续的所有工作都是http包来完成的.我们肯定不能去魔改http包
* 缺乏控制力:如果将来希望支持优雅退出的功能,将难以支持
  * 只能通过`http.Server.Shutdown()`/`http.Server.Close()`等http包里的方法去实现,没办法自己实现.因为这样的设计即使你给自定义Server接口的实现类设计了`Shutdown()`方法,也调用不到.本质上还是因为**从端口启动开始,后续的所有工作都是http包来完成的**,我们无法干涉这其中的步骤

### 4.3 构建Server Interface的方案:组合`http.Handler`并增加`Start()`方法

#### 4.3.1 增加`Start()`方法

在这个方案中,我们希望使用者调用我们的`Server.Start()`方法来启动HTTP服务器.

工程结构如下:

```
(base) root@yuanhong 02-composingHandlerAndAddStart % tree ./
./
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go

0 directories, 3 files
```

* `serverInterface.go`:定义Server接口

```go
package composingHandlerAndAddStart

import "net/http"

type Server interface {
	http.Handler
	Start(addr string) error
}
```

* `httpServer.go`:Server接口的HTTP实现

```go
package composingHandlerAndAddStart

import (
	"net"
	"net/http"
)

type HTTPServer struct {
}

func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO implement me
	panic("implement me")
}

func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}
```

可以看到,这样的设计就支持注册after start回调了.大白话来说就是:相比于直接调用`http.ListenAndServe()`的方案,这种方案拆解了监听和启动服务这2个步骤,使得在这2个步骤之间可以做一些操作了.

* `httpServer_test.go`:使用HTTP Server

```go
package composingHandlerAndAddStart

import "testing"

func TestServer(t *testing.T) {
	s := &HTTPServer{}
	s.Start(":8084")
}
```

#### 4.3.2 支持HTTPS

可以使用装饰器模式,基于`HTTPServer`结构体进行再封装:

工程结构如下:

```
(base) root@yuanhong 02-composingHandlerAndAddStart % tree ./
./
├── httpServer.go
├── httpServer_test.go
├── httpsServer.go
└── serverInterface.go

0 directories, 4 files
```

* `httpsServer.go`:Server接口的HTTPS实现

```go
package composingHandlerAndAddStart

type HTTPSServer struct {
	HTTPServer
}
```

这个不是现在的重点,只是提一下可以这么去设计.

#### 4.3.3 在`Start()`方法内部创建一个原生的`http.Server`对象

* 工程结构如下:

```
(base) root@yuanhong 02-composingHandlerAndAddStart % tree ./
./
├── createHttpServerInStart.go
├── httpServer.go
├── httpServer_test.go
├── httpsServer.go
└── serverInterface.go

0 directories, 5 files
```

* `createHttpServerInStart.go`:Server接口的HTTP实现

```go
package composingHandlerAndAddStart

import (
	"net"
	"net/http"
)

type HTTPServerWithOriginHttpServer struct {
}

func (s *HTTPServerWithOriginHttpServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO implement me
	panic("implement me")
}

func (s *HTTPServerWithOriginHttpServer) Start(addr string) error {
	originServer := http.Server{
		Addr:              addr,
		Handler:           s,
	}

	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	return originServer.Serve(l)
}
```

#### 4.3.4 组合`http.Handler`并增加`Start()`方法的优缺点

这样设计的优点:

* `Server`接口的实现既可以当成普通的`http.Handler`接口的实现来使用,又可以作为一个独立的实体,拥有自己的管理生命周期的能力
* 完全的控制，可以为所欲为

这样设计的缺点:

* 如果用户不希望通过调用`http.ListenAndServeTLS()`的方式来实现HTTPS支持,那么`Server`接口需要提供HTTPS的支持(也就是HTTPS的实现)

## PART5. ServeHTTP的实现

`ServeHTTP()`方法是处理请求的入口.这里说的"入口"指的是你设计的WEB框架的入口.在这个方法中需要完成3个操作:

* **Context构建**
* **路由匹配**
* **执行业务逻辑**

## PART6. 注册路由API设计

上文说过,`ServeHTTP()`方法作为WEB框架的入口,需要完成3个操作:

* **Context构建**:还没设计了先不考虑
* **路由匹配**:首先需要把路由注册到Server上,才能考虑匹配的过程
* **执行业务逻辑**:这里也先不考虑

首先需要在接口中定义路由注册的行为.参考GIN的IRoutes接口/Iris的APIBuilder结构体/Echo的Echo结构体,他们都是把注册路由的行为定义在接口上的.

在之前的课程分析GIN框架时说过,实际上Engine结构体的`GET()`/`POST()`等HTTP动词方法,本质上调用的都是`Handle()`方法.所以注册路由的方法分为2类:

* **针对任意方法的**:如Gin和Iris的`Handle()`方法、Echo的`Add()`方法
* **针对不同HTTP方法的**:如`GET()`、`POST()`、`DELETE()`这一类方法基本上都是委托给前一类方法

所以实际上核心方法只需要有一个,例如`Handle()`方法.其它的方法都建立在这上面.

## PART7. AddRoute方法

在本例中,我们将上文的`Handle()`方法命名为`AddRoute()`方法.从命名上来讲,`Handle()`表示"处理"的含义,而实际上这个方法要完成的操作是**注册路由**,而非处理,因此用`AddRoute()`更合适.

### 7.1 定义注册路由行为

* 工程结构如下:

```
(base) root@yuanhong 03-serveHTTP % tree ./
./
├── handleFunc.go	// 定义业务逻辑函数类型
├── httpServer.go	// 定义HTTP服务器
└── serverInterface.go	// 定义HTTP服务器接口

0 directories, 3 files
```

* `handleFunc.go`:定义业务逻辑函数类型

```go
package serveHTTP

// HandleFunc 定义业务逻辑函数类型
// Tips: 该类型应与http.HandlerFunc类型一致 此处只是暂时定义一下这个类型
type HandleFunc func()
```

注:此处将其命名为`HandleFunc`而非`HandlerFunc`,是因为`handle`是动词而`handler`是名词,后边跟的`Func`很明显是名词.使用动名词的组合更加符合GO的命名风格.

* `serverInterface.go`:定义HTTP服务器接口

```go
package serveHTTP

import "net/http"

// Server WEB服务器接口
type Server interface {
	// Handler 组合http.Handler接口
	http.Handler

	// Start 启动WEB服务器
	Start(addr string) error

	// AddRoute 注册路由
	AddRoute(method string, path string, handleFunc HandleFunc)
}
```

* `httpServer.go`:定义HTTP服务器

```go
package serveHTTP

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO implement me
	panic("implement me")
}

// Start 启动WEB服务器
func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}

// AddRoute 注册路由
func (s *HTTPServer) AddRoute(method string, path string, handleFunc HandleFunc) {
	// TODO: implement me
	panic("implement me")
}
```

### 7.2 定义Context

其实相比7.1小节,就是增加了一个Context类型的定义,并修改了HandleFunc类型的入参

* 工程结构如下:

```go
(base) root@yuanhong 03-serveHTTP % tree ./
./
├── context.go	// 定义HandleFunc的上下文
├── handleFunc.go
├── httpServer.go
└── serverInterface.go

0 directories, 4 files
```

* `context.go`:定义HandleFunc的上下文

```go
package serveHTTP

// Context HandleFunc的上下文
type Context struct {
}
```

* `handleFunc.go`:

```go
package serveHTTP

// HandleFunc 定义业务逻辑函数类型
// Tips: 该类型应与http.HandlerFunc类型一致 此处只是暂时定义一下这个类型
type HandleFunc func(ctx Context)
```

其他2个文件没有变化.

### 7.3 思考:是否需要`AddRoutes()`方法?

#### 7.3.1 如果需要AddRoutes()方法,该如何实现?

来看GIN的`Engine.Handle()`方法:

```go
func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
	if matched := regEnLetter.MatchString(httpMethod); !matched {
		panic("http method " + httpMethod + " is not valid")
	}
	return group.handle(httpMethod, relativePath, handlers)
}
```

可以看到该方法是支持**1次注册多个逻辑处理函数到路由上的**.如果我们要是也支持这个功能,该如何实现?

* 工程结构如下:

```
(base) root@yuanhong 04-addRoutes % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
└── serverInterface.go

0 directories, 4 files
```

* `serverInterface.go`:

```go
package addRoutes

import "net/http"

// Server WEB服务器接口
type Server interface {
	// Handler 组合http.Handler接口
	http.Handler

	// Start 启动WEB服务器
	Start(addr string) error

	// AddRoute 注册路由
	AddRoute(method string, path string, handleFunc HandleFunc)

	// AddRoutes 支持1个路由对应多个处理函数的注册路由
	AddRoutes(method string, path string, handles ...HandleFunc)
}
```

* `httpServer.go`:

```go
package addRoutes

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO implement me
	panic("implement me")
}

// Start 启动WEB服务器
func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}

// AddRoute 注册路由
func (s *HTTPServer) AddRoute(method string, path string, handleFunc HandleFunc) {
	// TODO: implement me
	panic("implement me")
}

// AddRoutes 支持1个路由对应多个处理函数的注册路由
func (s *HTTPServer) AddRoutes(method string, path string, handles ...HandleFunc) {
	// TODO: implement me
	panic("implement me")
}
```

#### 7.3.2 从使用的角度思考是否需要AddRoutes()方法

写个单元测试,演示一下如何使用`AddRoutes()`方法:

* 工程结构如下:

```
(base) root@yuanhong 04-addRoutes % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go		// 演示如何调用Server实现的单测
└── serverInterface.go

0 directories, 5 files
```

* `httpServer_test.go`:演示如何调用Server实现的单测

```go
package addRoutes

import (
	"fmt"
	"net/http"
	"testing"
)

func TestServer(t *testing.T) {
	s := &HTTPServer{}

	// 注册多个处理函数到路由
	handleFoo := func(ctx Context) { fmt.Println("处理第1件事") }
	handleBar := func(ctx Context) { fmt.Println("处理第2件事") }

	s.AddRoutes(http.MethodGet, "/getUser", handleFoo, handleBar)

	_ = s.Start(":8080")
}
```

其他文件无变化.

#### 7.3.3 尝试使用AddRoute()方法实现同等效果

完全可以将7.3.2示例中的`handleFoo`和`handleBar`封装成1个函数,然后调用`AddRoute()`方法,其实是可以达到同等效果的.

* 工程结构如下:

```
(base) root@yuanhong 03-serveHTTP % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go		// 演示如何调用Server实现的单测
└── serverInterface.go

0 directories, 5 files
```

* `httpServer_test.go`:演示如何调用Server实现的单测

```go
package serveHTTP

import (
	"fmt"
	"net/http"
	"testing"
)

func TestServer(t *testing.T) {
	s := &HTTPServer{}

	// 注册1个处理函数到路由
	handleFoo := func(ctx Context) { fmt.Println("处理第1件事") }
	handleBar := func(ctx Context) { fmt.Println("处理第2件事") }

	// 将2个处理函数封装为1个处理函数
	handleAssemble := func(ctx Context) {
		handleFoo(ctx)
		handleBar(ctx)
	}

	s.AddRoute(http.MethodGet, "/getUser", handleAssemble)

	_ = s.Start(":8080")
}
```

也就是说,**让使用者自行将多个处理函数封装为1个,然后交给WEB框架注册路由**.

#### 7.3.4 结论

不需要支持`AddRoutes()`方法.

理由:把"将多个函数组合成1个函数"的职责交由使用者去实现即可

而且,如果需要支持`AddRoutes()`方法,还会带来一些其他问题:

1. 如果允许注册多个函数,那么在实现的时候就要考虑,**若其中一个HandleFunc执行失败了,是否还允许继续执行后续的HandleFunc?反之,如果其中一个HandleFunc要中断后续执行,该怎么中断?**
   * 中断后续执行:例如共有5个HandleFunc,但执行完第2个之后,业务逻辑上判断后续的HandleFunc不需要执行了,即为中断后续执行
2. 站在使用者的视角上来看,由于`AddRoutes()`方法中的`handles`是不定长参数,因此使用时可能出现一个函数都不传的情况.这种情况在编译期间不会被发现

另外,Echo框架虽然不支持传入多个注册函数的功能,但它保留了传入的HandleFunc为nil的可能性.实际上Echo是**将中间件注册逻辑和路由注册逻辑合并在了一起**:

```go
// GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware.
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
	return e.Add(http.MethodGet, path, h, m...)
}
```

### 7.4 AddRoute方法的衍生方法

针对不同HTTP方法的注册API,都可以**委托给`AddRoute()`方法**.这种设计思路很常用.

在我们的设计中,我们**不认为针对HTTP动词的路由注册方法是框架的核心方法**.**保持接口简洁,但这并不意味着实现不能复杂**.

因此,我们将`GET()`/`POST()`等方法实现在`HTTPServer`结构体上.

* 工程结构如下:

```
(base) root@yuanhong 03-serveHTTP % tree ./     
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go

0 directories, 5 files
```

* `httpServer.go`:

```go
package serveHTTP

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// TODO implement me
	panic("implement me")
}

// Start 启动WEB服务器
func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}

// AddRoute 注册路由
func (s *HTTPServer) AddRoute(method string, path string, handleFunc HandleFunc) {
	// TODO: implement me
	panic("implement me")
}

// GET 注册GET请求路由
func (s *HTTPServer) GET(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodGet, path, handleFunc)
}

// POST 注册POST请求路由
func (s *HTTPServer) POST(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodPost, path, handleFunc)
}
```

其他文件代码无变化.

这样设计的话,就需要注意在使用时,初始化HTTPServer结构体时,不能将其类型声明为Server接口:

```go
package serveHTTP

import (
	"fmt"
	"net/http"
	"testing"
)

func TestServer(t *testing.T) {
	// Tips: 初始化HTTPServer结构体时,不能将其类型声明为Server接口的实现,因为一些方法被我们定义在了
	// Tips: 实现上,而不是在接口上,所以不能将其类型声明为Server接口的实现.即:不能写成如下形式:
	// var s Server = &HTTPServer{}
	s := &HTTPServer{}

	// 注册1个处理函数到路由
	handleFoo := func(ctx Context) { fmt.Println("处理第1件事") }
	handleBar := func(ctx Context) { fmt.Println("处理第2件事") }

	// 将2个处理函数封装为1个处理函数
	handleAssemble := func(ctx Context) {
		handleFoo(ctx)
		handleBar(ctx)
	}

	s.AddRoute(http.MethodGet, "/getUser", handleAssemble)

	_ = s.Start(":8080")
}
```

`AddRoute()`方法最终会和路由树交互,我们后面再考虑.

![框架设计思想](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-aa562307ee1fc90cea620d253667a1faf9fab459%2F%E6%A1%86%E6%9E%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E6%83%B3.png?alt=media)

如上图示,**保持核心API的方法较少,而衍生API的方法较多**,是我们这个框架的核心设计思想.因此在设计的时候,需要区分核心API和衍生API.

## PART8. `ServeHTTP()`方法

`ServeHTTP()`方法是作为`http`包与WEB框架的关联点,需要在`ServeHTTP()`方法内部完成如下操作:

* **构建起WEB框架的上下文**
* **查找路由树,并执行命中路由的代码**

说句再直白一点的话:请求进来了就执行`ServeHTTP()`方法

### 8.1 定义Context

那么问题来了,一个HandleFunc的上下文是什么?首先至少肯定有**请求**和**响应**.因此我们的Context就需要能够将**请求**和**响应**"带"到具体的HandleFunc中去.

* 工程结构如下:

```
(base) root@yuanhong 03-serveHTTP % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go

0 directories, 5 files
```

* `context.go`:

```go
package serveHTTP

import "net/http"

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
}
```

* `httpServer.go`:修改了`ServeHTTP()`方法的代码.增加了构建上下文的部分.

```go
package serveHTTP

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 查找路由树并执行命中的业务逻辑
	s.serve(ctx)

	// TODO implement me
	panic("implement me")
}

// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context)  {
	// TODO implement me
	panic("implement me")
}

// Start 启动WEB服务器
func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}

// AddRoute 注册路由
func (s *HTTPServer) AddRoute(method string, path string, handleFunc HandleFunc) {
	// TODO: implement me
	panic("implement me")
}

// GET 注册GET请求路由
func (s *HTTPServer) GET(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodGet, path, handleFunc)
}

// POST 注册POST请求路由
func (s *HTTPServer) POST(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodPost, path, handleFunc)
}
```

## PART9. 面试要点

### 9.1 HTTP服务器的生命周期?

一般来说就是启动、运行和关闭.在这三个阶段的前后都可以插入生命周期回调.一般来说,面试生命周期,多半都是为了后边问生命周期回调.例如说怎么做WEB服务的服务发现?就是利用生命周期回调的启动后回调,将WEB服务注册到服务中心.

### 9.2 HTTP Server的功能?

记住在不同的框架里面有不同的叫法.比如说在Gin里面叫做Engine,它们的基本功能都是提供路由注册、生命周期控制以及作为与http包结合的桥梁.

## 附录

### 问题1. 生命周期回调究竟是个啥?


# PART03.路由树


# 3.01 路由树-Beego\&GIN\&Echo实现与设计总结

## PART1. 学习路线

![学习路线](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-6694f38bd37b3e8389e4c4cb971f6652fee1fa4b%2F%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF.png?alt=media)

我们分成以下步骤来实现一颗路由树:

* 全静态匹配
* 支持通配符匹配
* 支持参数路由
* 作业:支持正则匹配

## PART2. Beego的路由树实现

### 2.1 ControllerRegister

在之前的框架概览部分讲到过,Beego的`ControllerRegister`结构体负责路由相关的功能.

`ControllerRegister`:类似于容器,存储了**所有**的路由树.

```go
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
	routers      map[string]*Tree
	enablePolicy bool
	enableFilter bool
	policies     map[string]*Tree
	filters      [FinishRouter + 1][]*FilterRouter
	pool         sync.Pool

	// the filter created by FilterChain
	chainRoot *FilterRouter

	// keep registered chain and build it when serve http
	filterChains []filterChainConfig

	cfg *Config
}
```

`ControllerRegister.routers`即为路由树.可以看到其类型为`map[string]*Tree`,其中的key表示HTTP动词.也就是说,Beego的路由树是**按照HTTP动词来组织的**,每个HTTP动词对应一棵路由树.因此说`ControllerRegister`存储了**所有**的路由树.

### 2.2 Tree

`Tree`:它代表的就是路由树,在Beego中,一棵路由树可以被看做由子树组成.`Tree`实际上是一种多叉树的数据结构.

```go
// Tree has three elements: FixRouter/wildcard/leaves
// fixRouter stores Fixed Router
// wildcard stores params
// leaves store the endpoint information
type Tree struct {
	// prefix set for static router
	prefix string
	// search fix route first
	fixrouters []*Tree
	// if set, failure to match fixrouters search then search wildcard
	wildcard *Tree
	// if set, failure to match wildcard search
	leaves []*leafInfo
}
```

其中:

* `fixrouters`:静态路由
* `wildcard`:通配符路由
* `leaves`:叶子节点

### 2.3 leafInfo

Beego的路由树中区分了叶子节点和非叶子节点,但可能实际意义并不大.`leafInfo`结构体表示了叶子节点:

```go
type leafInfo struct {
	// names of wildcards that lead to this leaf. eg, ["id" "name"] for the wildcard ":id" and ":name"
	wildcards []string

	// if the leaf is regexp
	regexps *regexp.Regexp

	runObject interface{}
}
```

### 2.4 Beego设计抽象

如下图示,Beego的树定义,并没有采用children式的定义,而是采用**递归式的定义,即:一棵树由根节点+子树组成**.也就是说实际上`ControllerRegister.routers`实际上是路由森林而非路由树

![Beego设计抽象](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-f80225eaaa0ffb2ed7acac2fd3029dfec01da253%2FBeego%E8%AE%BE%E8%AE%A1%E6%8A%BD%E8%B1%A1.png?alt=media)

## PART3. GIN的路由树实现

### 3.1 methodTrees

在之前的框架概览部分讲到过,GIN的路由树功能本质上是依赖于`Engine`结构体的`trees`字段的

```go
type Engine struct {
	// some other fields
	trees            methodTrees
	// some other fields
}
```

methodTrees的定义:

```go
type methodTrees []methodTree
```

`methodTrees`类型是`methodTree`的切片.实际上GIN的路由树也是**按照HTTP动词来组织的**,每个HTTP动词对应一棵路由树.只是相比于Beego使用了`map[string]*Tree`的方式存储路由森林,GIN使用了`[]methodTree`,也就是切片的方式来存储路由森林,而实际上这个切片中元素的数量只是和HTTP动词的数量(9个)相等.该切片中的每个元素表示了一棵路由树.

### 3.2 methodTree

```go
type methodTree struct {
	method string
	root   *node
}
```

`methodTree`定义了单棵路由树.树在GIN里边采用的是children的定义方式,即**树由节点构成**.

* `methodTree.method`:HTTP动词
* `methodTree.root`:根节点

### 3.3 node

`node`结构体表示树上的1个节点:

```go
type node struct {
	path      string
	indices   string
	wildChild bool
	nType     nodeType
	priority  uint32
	children  []*node // child nodes, at most 1 :param style node at the end of the array
	handlers  HandlersChain
	fullPath  string
}
```

其中:

* `children`字段表示该节点的子节点
* `nType`:实际上是一个uint8类型,用于标记节点类型
* `wildChild`:标记特殊节点
* `handlers`:责任链

### 3.4 GIN设计抽象

**GIN利用路由的最长公共前缀来构造路由树**.

![GIN设计抽象](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-8c95dc8521d13841c6ae09a7eb1dfc6844c274e5%2FGIN%E8%AE%BE%E8%AE%A1%E6%8A%BD%E8%B1%A1.jpg?alt=media)

* 图中的`??`表示特殊字符

所谓的**最长公共前缀**,指的是:

* /
  * c
    * o
      * ntact
      * m
  * a
    * b
  * doc/
    * go(注意`go_faq.html`和`go1.html`的最长公共前缀是`go`而不是`g`,因此基址是`go`)
      * \_faq.html
      * 1.html
    * ??(表示路由中的特殊字符)
      * α
      * β
  * hi

这个数据结构也被称为前缀树.

## PART4. Echo的路由树实现

### 4.1 Router

在之前的框架概览部分讲到过,Echo结构体中的`routers`字段表示根据Host将路由树隔离,可以看做是类似namespace之类的概念,既是一种组织方式,也是一种隔离机制;而`router`字段表示路由树

```go
type Echo struct {
	// some other fields
	router           *Router
	routers          map[string]*Router
	// some other fields
}
```

**注意和Beego的HTTP动词不同,`Echo.routers`字段的`map[string]*Router`中,key表示Host**

而`Router`结构体表示路由注册中心,其中维护了路由树(`routes`字段)

```go
Router struct {
	tree   *node
	routes map[string]*Route
	echo   *Echo
}
```

### 4.2 Route

`Route`结构体表示具体的路由:

```go
Route struct {
	Method string `json:"method"`
	Path   string `json:"path"`
	Name   string `json:"name"`
}
```

### 4.3 node

`node`结构体也是常规的树节点设计,内部也采用的是`children`字段来表示子节点的数据

```go
node struct {
	kind           kind
	label          byte
	prefix         string
	parent         *node
	staticChildren children
	originalPath   string
	methods        *routeMethods
	paramChild     *node
	anyChild       *node
	paramsCount    int
	// isLeaf indicates that node does not have child routes
	isLeaf bool
	// isHandler indicates that node has at least one handler registered to it
	isHandler bool

	// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
	notFoundHandler *routeMethod
}
```

与GIN的`node`结构体基本类似,只是相比于GIN,Echo的`node`结构体多维护了一个`parent`字段,即指向父节点的指针.其中:

* `staticChildren`:静态子节点
* `paramChild`:参数子节点
* `anyChild`:任意子节点(通配符匹配)

有一种观点认为:Echo的设计不如GIN的设计,也是因为Echo多引入了一些抽象,但是实际上它们功能都差不多.

**开源,讲究的是一个以简为美;但是工作,讲究刷KPI唬人为要.因此开源要克制,而工作要泛滥**.

当然,我现在连刷KPI的机会都没有\~

## PART5. 路由树设计总结

* 归根结底就是设计一颗**多叉树**
* 我们的设计也是**按照HTTP方法来组织路由树**,每个HTTP方法一棵树
* 每个节点维护自己的子节点
* 以下这两种形态的路由树组织,性能差异不大,但是第二种的实现要简单很多

第1种:最长公共前缀树

![GIN设计抽象](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-8c95dc8521d13841c6ae09a7eb1dfc6844c274e5%2FGIN%E8%AE%BE%E8%AE%A1%E6%8A%BD%E8%B1%A1.jpg?alt=media)

GIN的路由树不好读懂的原因就在于提取公共前缀.因为每次注册路由时,最长公共前缀都会发生变化.举个不太恰当的例子:

`5 10 15 18`这4个数字的最大公约数为3;而将18换为45,则`5 10 15 45`的最大公约数为5.每当有一个数字被替换或添加,都要重新计算这一组数字的最大公约数.

[GIN中根据最长公共前缀调整路由树结构的实现](https://github.com/gin-gonic/gin/blob/master/tree.go#L165)

[测试用例](https://github.com/gin-gonic/gin/blob/master/tree_test.go#L101)

在[这里](https://github.com/gin-gonic/gin/blob/master/tree_test.go#L121)打一个断点就能看到他实现的树的结构

第2种:按`/`"分层"的树

![比较符合常规认知的路由树](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-55e522793d0c75acf3d3555d15f7c6885088348c%2F%E6%AF%94%E8%BE%83%E7%AC%A6%E5%90%88%E5%B8%B8%E8%A7%84%E8%AE%A4%E7%9F%A5%E7%9A%84%E8%B7%AF%E7%94%B1%E6%A0%91.jpg?alt=media)

## 附录

### Beego的路由树

大体上理解,Beego的路由树,是直接将叶子节点append到fixrouters和wildcard上的;而GIN的路由树,是通过递归来维护树的内部节点与内部节点的关系的

![Beego设计抽象](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-f913cd9a5c9b358eb40d0abfb7a30ad9d07404d7%2FBeego%E8%AE%BE%E8%AE%A1%E6%8A%BD%E8%B1%A1.jpg?alt=media)

也就是说Beego的路由树,实际上只有3层:

* 根节点
* 静态路由&通配符匹配
* 根节点


# 3.02 路由树-全静态匹配

我们利用全静态匹配来构建路由树,后面再考虑**重构路由树**以支持通配符匹配、参数路由等复杂匹配

所谓的**静态匹配,就是路径的每一段都必须严格相等**

## PART1. 接口设计

这里我们按照上节课说的,设计一种比较符合常规认知的路由树.

如同Beego中的`HttpServer`结构体和`ControllerRegister`结构体不是一个结构体;GIN中的`Engine`结构体和`IRoutes`接口的实现不是一个结构体一样.在我们的框架中,表示路由树的抽象也不应该是`HTTPServer`结构体的一个字段,而应该是一个单独的结构体.

之前我们定义了`HttpServer.AddRoute()`方法,用于注册路由.但实际上注册路由并不是`HTTPServer`的职责.`HTTPServer`是代表服务器的抽象,而非是代表路由树的抽象.

* 修改前的工程结构:

```
(base) yanglei@yuanhong 05-designRoute % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go

0 directories, 5 files
```

[修改前代码](https://github.com/rayallen20/GoInAction/tree/master/code/week1/server/v1)即为v1版本

### 1.1 带有子树的路由树结构

这里提前开个上帝视角给一个结论:这样设计的意义不大.因为即使设计成有子树结构的路由树,最终大部分的操作还是会落在节点上,而不会落在子树上.但还是演示一下这样的代码组织

#### 1.1.1 定义路由森林

创建文件`router.go`:

```go
package designRouteWithChildTree

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树
	// 即: 每个HTTP动词对应一棵路由树
	trees map[string]tree
}
```

这里路由森林的设计和GIN/Beego的一样,没什么可讲的

#### 1.1.2 定义子树

创建文件`tree.go`:

```go
package designRouteWithChildTree

// tree 路由树
type tree struct {
	// root 树的根节点
	root *node
}
```

这里也是一样,每棵树有一个指向根节点的指针,也没什么可讲的

#### 1.1.3 定义节点

创建文件`node.go`:

```go
package designRouteWithChildTree

// node 路由树的节点
type node struct {
	// path 当前节点的路由路径
	path string

	// children 子路由路径到子节点的映射
	children map[string]*node

	// HandleFunc 路由对应的业务逻辑
	HandleFunc
}
```

这里需要画图演示一下:

![node节点图示](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-97b25852e18f51913f6f7bed294f72444a72a7be%2Fnode%E8%8A%82%E7%82%B9%E5%9B%BE%E7%A4%BA.jpg?alt=media)

看到这样的代码组织,可以想象到大部分的操作还是会落在`node`结构体上.因此在`node`和`routers`之间定义一层`tree`,意义并不大.

完整工程结构如下:

```
(base) yanglei@yuanhong 05-designRoute % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── node.go
├── router.go
├── serverInterface.go
└── tree.go

0 directories, 8 files
```

### 1.2 路由森林直接指向树的根节点的路由树结构

初态工程结构如下:

```
(base) yanglei@yuanhong 06-designRoute % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go

0 directories, 5 files
```

还是v1的代码.

#### 1.2.1 定义路由森林

创建文件`router.go`:

```go
package designRoute

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}
```

#### 1.2.2 定义节点

创建文件`node.go`:

```go
package designRoute

// node 路由树的节点
type node struct {
	// path 当前节点的路径
	path string
	
	// children 子路由路径到子节点的映射
	children map[string]*node
	
	// HandleFunc 路由对应的业务逻辑
	HandleFunc
}
```

#### 1.2.3 定义注册路由的方法

该方法负责将路由注册到对应的路由树上,当然此时我们还没有实现这个功能.实际上这个方法应该定义在`router`结构体上,而非`HTTPServer`结构体上.因为这是`router`结构体的职责.

`router.go`:

```go
package designRoute

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// AddRoute 注册路由到路由森林中的路由树上
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// TODO: implement me
	panic("implement me")
}
```

#### 1.2.4 HTTPServer组合router

`httpServer.go`:

```go
package designRoute

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
	*router
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 查找路由树并执行命中的业务逻辑
	s.serve(ctx)

	// TODO implement me
	panic("implement me")
}

// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context) {
	// TODO implement me
	panic("implement me")
}

// Start 启动WEB服务器
func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}

// GET 注册GET请求路由
func (s *HTTPServer) GET(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodGet, path, handleFunc)
}

// POST 注册POST请求路由
func (s *HTTPServer) POST(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodPost, path, handleFunc)
}
```

此处做了2处修改:

1. `HTTPServer`结构体添加了一个匿名字段`*router`
2. `HTTPServer`结构体删除了`AddRoute()`方法
   * 但此时`HTTPServer`结构体仍然是`Server`接口的实现,因为`HTTPServer`结构体组合了`router`结构体,`router`结构体实现了`AddRoute()`方法
   * Tips:如果想要在`HTTPServer`结构体中使用命名字段来组合`router`,则还是需要实现`HTTPServer`结构体的`AddRoute()`方法,只不过是在该方法中调用`router.`AddRoute()\`

#### 1.2.5 定义创建路由森林的函数

这里其实返回指针还是实例无所谓,因为数据最终放在了一个map里边.无论你通过实例访问该map还是通过指针访问该map,因为map是引用类型,所以最终二者效果一致.

`router.go`:

```go
package designRoute

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// TODO: implement me
	panic("implement me")
}
```

#### 1.2.6 定义创建HTTPServer的函数

`httpServer.go`:

```go
package designRoute

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
	*router
}

// NewHTTPServer 创建HTTP服务器
func NewHTTPServer() *HTTPServer {
	return &HTTPServer{
		router: newRouter(),
	}
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 查找路由树并执行命中的业务逻辑
	s.serve(ctx)

	// TODO implement me
	panic("implement me")
}

// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context) {
	// TODO implement me
	panic("implement me")
}

// Start 启动WEB服务器
func (s *HTTPServer) Start(addr string) error {
	l, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	// 在监听端口之后,启动服务之前做一些操作
	// 例如在微服务框架中,启动服务之前需要注册服务

	return http.Serve(l, s)
}

// GET 注册GET请求路由
func (s *HTTPServer) GET(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodGet, path, handleFunc)
}

// POST 注册POST请求路由
func (s *HTTPServer) POST(path string, handleFunc HandleFunc) {
	s.AddRoute(http.MethodPost, path, handleFunc)
}
```

此处做了1处修改:

* 新增函数`NewHTTPServer()`

完整的工程结构如下:

```
(base) yanglei@yuanhong 06-designRoute % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── node.go		// 定义节点
├── router.go		// 定义路由森林
└── serverInterface.go

0 directories, 7 files
```


# 3.03 路由树-TDD起步

在有了类型定义之后,我们就可以考虑按照TDD的思路,用测试来驱动我们的实现.

我们使用简化版的TDD,即:

1. 定义API(这一步上节课已经定义好了)
2. 定义测试
3. 添加测试用例
4. 实现,并且确保实现能够通过测试用例
5. 重复3-4直到考虑了所有的场景
6. 重复步骤1-5

## PART1. 定义测试文件

初态工程结构如下:

```
(base) yanglei@yuanhong 07-TDD % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── node.go
├── router.go
└── serverInterface.go

0 directories, 7 files
```

我们本节课主要是针对路由树进行测试,实际上是要测试`router.AddRoute()`方法注册路由的结果是否符合预期.因此创建文件`router_test.go`

```
(base) yanglei@yuanhong 07-TDD % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── node.go
├── router.go
├── router_test.go		// router.go的测试文件
└── serverInterface.go

0 directories, 8 files
```

### 1.1 构造与验证路由树

我们要测试的是注册路由后,路由树的结构和预期是否相同,因此测试分为2个步骤:

1. 构造路由树
2. 验证路由树

`router_test.go`:

```go
package tdd

import "testing"

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}

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

	for _, testRoute := range testRoutes {
		r.AddRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}
	
	// step2. 验证路由树
}
```

此处我们其实并不关心HandleFunc,因此也就没有在`TestNode`结构体上定义这个字段,调用`router.AddRoute()`方法时也是传入了一个mock值

### 1.2 断言路由树

注意断言的时候我们无法使用`assert.Equal()`,因为`HandleFunc`类型不是可比较的.也就是说以下代码是不行的:

```go
package tdd

import (
	"github.com/stretchr/testify/assert"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	assert.Equal(t, wantRouter, r)
}
```

因此我们需要自定义判断二者是否相等的方法.

### 1.2.1 断言router是否相等

此处我们需要判断2个router是否相等.逻辑上很简单:

* 如果两个路由森林中的路由树数量不同,则不相等
* 如果目标router中没有对应HTTP方法的路由树,则不相等
* 比对相同HTTP方法的路由树结构是否相等.这个方法下一小节实现

```go
package tdd

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	assert.Equal(t, wantRouter, r)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, ok := tree.equal(dstTree)
	}
	return "", true
}

// equal 比较两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {

}
```

### 1.2.2 断言node是否相等

虽然上述代码的注释中写的是"比对两棵路由树的结构是否相等",但实际上它们都是`node`结构体的实例,因此在`node`结构体上实现`equal()`方法即可.

断言node是否相等的逻辑就稍微复杂一些:

* 如果目标节点为nil,则不相等
* 如果两个节点的path不相等,则不相等
* 若两个节点的子节点数量不相等,则不相等
* 若两个节点的handleFunc类型不同,则不相等(TODO:反射我用的很少,这块代码是直接抄的)
* 比对两个节点的子节点映射是否相等
  * 如果源节点的子节点中,存在目标节点没有的子节点,则不相等
  * 两个path相同的节点再次递归比对

```go
package tdd

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 比对handleFunc
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

## PART2. 添加测试用例

假定我们注册了一个名为`/user/home`的路由,那么我们预期的路由树结构应该如下图示:

![预期路由树结构](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-8c0630e31d6bc8696000f0daa2d5d7e3444b20bb%2F%E9%A2%84%E6%9C%9F%E8%B7%AF%E7%94%B1%E6%A0%91%E7%BB%93%E6%9E%84.jpg?alt=media)

```go
package tdd

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:     "home",
								children: nil,
								// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
								// /user和/ 都是没有handleFunc的
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

需要注意的是,只有"home"节点是有HandleFunc的.因为最终路由是注册在"home"节点上的.

## PART3. 实现AddRoute方法

### 3.1 查找或创建子节点

#### 3.1.1 根据method查找根节点

这一步比较简单,根据根据method查找根节点,不存在则创建

`router.go`:

```go
package tdd

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}
}
```

#### 3.1.2 在根节点上查找目标节点

这里就需要考虑:

* 若在根节点的children映射中查找到了目标节点,则添加HandleFunc后返回
* 若在根节点的children映射中没有查找到子节点,则创建目标节点
  * 树中途存在未被创建的节点,则创建该节点,然后以该节点为目标节点,继续创建子节点,直到找到目标节点为止
  * 为目标节点添加HandleFunc

`node.go`:

```go
package tdd

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

到这一步,就可以跑一下单测了

## 附录

### 发现自身的问题

1. 这里老师debug的过程是比我日常开发的方式要强很多的,我还是最古老的`fmt.Printf()`的方式
   * 学人家怎么用IDE去Debug的
2. 但更深层次的原因是,他写了单测,才能支持他把代码运行起来.我以后也得考虑用这种方式开发,会高效很多


# 3.04 路由树-静态匹配测试用例

上节课已经跑通了第1个测试用例,这里再重申一次TDD的步骤:

1. 定义API
2. 定义测试
3. 添加测试用例
4. 实现,并且确保实现能够通过测试用例
5. 重复3-4直到考虑了所有的场景
6. 重复步骤1-5

接下来要做的就是不断添加测试用例.

## PART1. 添加测试用例

### 1.1 添加根节点的测试用例

为什么要单独给根节点添加测试用例?

注意我们的`AddRoute()`方法中有这样一段代码:

```go
// step1. 找到路由树
root, ok := r.trees[method]
// 如果没有找到路由树,则创建一棵路由树
if !ok {
	root = &node{
		path: "/",
	}
	r.trees[method] = root
}
```

可以看到逻辑上对根节点是有特殊处理的.

* 初态工程结构如下:

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

0 directories, 8 files
```

### 1.1.1 为根节点添加测试用例

`router_test.go`:

```go
package staticMatchingTestCase

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:     "home",
								children: nil,
								// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
								// /user和/ 都是没有handleFunc的
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
		},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此处改动了2处:

* 添加了1个测试用例
* `wantRouter`中为根节点添加了HandleFunc

其实稍微想一下就知道,我们的测试用例现在肯定跑不起来,因为我们在`AddRoute()`方法中把`path`的前导`/`给直接删掉了.导致按`/`切割`path`的结果是一个`[]string{""}`.HandleFunc加的位置不对.

### 1.1.2 为根节点添加特殊处理的代码

修这个bug的思路很简单:如果path是`/`,直接添加HandleFunc并返回即可

注意:这里也是通过IDE的Debug功能调试的

Tips:

* 注意Debug时控制台的`Resume Program`按钮是指执行下一次函数的意思.这个在调试时很有用
* Debug时可以点变量值左侧的调用栈看每个函数里当时的情况

`router.go`:

```go
package staticMatchingTestCase

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		root.HandleFunc = handleFunc
		return
	}

	// step3. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

这样就可以通过测试了.

### 1.2 添加前导`/user`节点的测试用例

`router_test.go`:

```go
package staticMatchingTestCase

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:     "home",
								children: nil,
								// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
								// /user和/ 都是没有handleFunc的
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
				},
				HandleFunc: mockHandleFunc,
			},
		},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此处改动了2处:

* 添加了1个测试用例
* `wantRouter`中为`user`节点添加了HandleFunc

这次是可以成功通过测试的.

### 1.3 添加`/order/detail`节点的测试用例

该用例是为了测试当路由树中间有不存在的节点(即`order`节点)时,`AddRoute()`方法是否符合预期.

`router_test.go`:

```go
package staticMatchingTestCase

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:     "home",
								children: nil,
								// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
								// /user和/ 都是没有handleFunc的
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
		},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此处改动了2处:

* 添加了1个测试用例
* `wantRouter`中添加了`order`节点,并为该节点设置了子节点`detail`,且为`detail`子节点添加了HandleFunc

这次是可以成功通过测试的.

### 1.4 为其他HTTP动词添加测试用例

此处为POST方法的`/order/create`路由添加测试用例.

`router_test.go`:

```go
package staticMatchingTestCase

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodPost,
			path:   "/order/create",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			// GET方法路由树
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:     "home",
								children: nil,
								// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
								// /user和/ 都是没有handleFunc的
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},

			// POST方法路由树
			http.MethodPost: &node{
				path: "/",
				children: map[string]*node{
					"order": &node{
						path: "order",
						children: map[string]*node{
							"create": &node{
								path:       "create",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此处改动了3处:

* 添加了1个测试用例
* `wantRouter`中添加了POST方法的路由树
* `wantRouter`中添加了`order`节点,并为该节点添加了子节点`create`,且为`create`子节点添加了HandleFunc

这次是可以成功通过测试的.

### 1.5 再次为POST方法添加测试用例

这个测试用例是为了测试POST方法中路由仅有1段的case.

添加`/login`路由的测试用例:

`router_test.go`:

```go
package staticMatchingTestCase

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用AddRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_AddRoute 测试路由注册功能的结果是否符合预期
func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodPost,
			path:   "/order/create",
		},
		{
			method: http.MethodPost,
			path:   "/login",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			// GET方法路由树
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": {
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:     "home",
								children: nil,
								// 注意路由是/user/home 因此只有最深层的节点才有handleFunc
								// /user和/ 都是没有handleFunc的
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},

			// POST方法路由树
			http.MethodPost: {
				path: "/",
				children: map[string]*node{
					"order": &node{
						path: "order",
						children: map[string]*node{
							"create": &node{
								path:       "create",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
					"login": &node{
						path:       "login",
						children:   nil,
						HandleFunc: mockHandleFunc,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	// HandleFunc类型是方法,方法不可比较,因此只能比较两个路由树的结构是否相等
	// assert.Equal(t, wantRouter, r)

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}

// equal 比较两个路由森林是否相等
// msg: 两个路由森林不相等时的错误信息
// ok: 两个路由森林是否相等
func (r *router) equal(y *router) (msg string, ok bool) {
	// 如果目标路由森林为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标路由森林为nil"), false
	}

	// 如果两个路由森林中的路由树数量不同 则不相等
	rTreesNum := len(r.trees)
	yTreesNum := len(y.trees)
	if rTreesNum != yTreesNum {
		return fmt.Sprintf("路由森林中的路由树数量不相等,源路由森林有 %d 棵路由树, 目标路由森林有 %d 棵路由树", rTreesNum, yTreesNum), false
	}

	for method, tree := range r.trees {
		dstTree, ok := y.trees[method]

		// 如果目标router中没有对应HTTP方法的路由树 则不相等
		if !ok {
			return fmt.Sprintf("目标 router 中没有HTTP方法 %s的路由树", method), false
		}

		// 比对两棵路由树的结构是否相等
		msg, equal := tree.equal(dstTree)
		if !equal {
			return method + "-" + msg, false
		}
	}
	return "", true
}

// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(y *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if y == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != y.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, y.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(y.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(y.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := y.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此处改动了2处:

* 添加了1个测试用例
* `wantRouter`中的POST路由树上添加了`login`节点,且为该节点添加了HandleFunc

这次是可以成功通过测试的.

## PART2. 全静态路由的非法用例

初态工程结构如下:

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

0 directories, 8 files
```

### 2.1 字面量为空字符串的路由

#### 2.1.1 添加对path为空字符串的限制

这里只需要在`AddRoute()`方法中限制`path`不能为空字符串即可:

`router.go`:

```go
package illegalTestCase

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

#### 2.1.2 添加对应的测试用例

测试用例如下:

`router_test.go`:

```go
// TestRouter_AddRoute_Illegal_Case 测试路由注册功能的非法用例
func TestRouter_AddRoute_Illegal_Case(t *testing.T) {
	r := newRouter()
	mockHandleFunc := func(ctx Context) {}

	// step1. 断言路由注册功能的非法用例
	// 1.1 测试路由为空字符串
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "", mockHandleFunc)
	}, "web: 路由不能为空字符串")
}
```

注意这时我们测试的是触发panic的用例,因此要使用`assert.Panics()`进行断言

注意

* 这时我们测试的是触发panic的用例,因此要使用`assert.Panics()`进行断言
* 文件还是`router_test.go`,只是我觉得再在`TestRouter_AddRoute()`函数上加测试用例就太乱了,而且本小节的目标就是对各种非法路由(或者可以说边缘case)做测试,所以我单独又写了一个测试函数.这里为了避免笔记看上去太乱了,所以只记录了新写的这个测试函数.

### 2.2 不是以`/`为前导字符的路由

从PART1的测试情况来看,似乎一切良好.但其实我们忽略了一个case:\*\*如果路由不是以`/`前导的怎么办?\*\*比如注册的路由为`login`,而不是`/login`的情况

注意:断点调试时,IDE上点(注意是拿鼠标点的不是拿代码打的)好断点之后,右键可以为断点设置条件,当符合条件时断点才生效.

![为断点设置条件](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-2d51f7e8c80d704c8e70fd4f00063ec1d2d6d565%2F%E4%B8%BA%E6%96%AD%E7%82%B9%E8%AE%BE%E7%BD%AE%E6%9D%A1%E4%BB%B6.png?alt=media)

注意:

这里因为我当时切割path时用的`strings.TrimLeft(path, "/")`,所以没触发这个case.

看起来似乎一切正常,甚至超出预期:我们还支持了以"前导字符不为`/`"的路由形式.此时再思考一个问题:**我们设计的框架究竟应不应该支持这种路由?**

我个人认为**不应该支持**.因为这样使用你的框架去写代码的人,他写出来的代码,可读性反而会变差.举个例子:

* 如果路由是`/login`,那么很容易从注册路由时的代码看出其路由的全貌
* 如果路由是`login`,那么阅读他的代码的人,也很自然的会去联想:
  * 这个`login`前边是不是还有什么前缀?
  * 会不会有一些类似namespace的隔离机制将"前导字符为`/`的路由"与"前导字符不为`/`的路由"分隔开?
  * "前导字符为`/`的路由"与"前导字符不为`/`的路由"有什么区别?

可以想象到,如果我们设计成支持"前导字符不为`/`的路由",反而做了一件费力不讨好的事情.

#### 2.2.1 对不是以`/`为前导字符的路由进行限制

这里只需要在`AddRoute()`方法中限制`path`必须以`/`开头即可

`router.go`:

```go
package illegalTestCase

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// 1.2 检测路由是否以"/"开头
	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

#### 2.2.2 添加对应的测试用例

`router_test.go`:

```go
// TestRouter_AddRoute_Illegal_Case 测试路由注册功能的非法用例
func TestRouter_AddRoute_Illegal_Case(t *testing.T) {
	r := newRouter()
	mockHandleFunc := func(ctx Context) {}

	// step1. 断言路由注册功能的非法用例
	// 1.1 测试路由为空字符串
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "", mockHandleFunc)
	}, "web: 路由不能为空字符串")

	// 1.2 测试路由不以"/"开头
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "login", mockHandleFunc)
	}, "web: 路由必须以 '/' 开头")
}
```

### 2.3 以`/`结尾的路由

#### 2.3.1 对以`/`结尾的路由进行限制

这里只需要在`AddRoute()`方法中限制`path`不能以`/`结尾即可

`router.go`:

```go
package illegalTestCase

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// 1.2 检测路由是否以"/"开头
	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	// 1.3 检测路由是否以"/"结尾
	// Tips: 这个逻辑判断放在根节点的处理后边确实是可以省点代码 但是我认为那样不太好理解
	// Tips: 我认为正常的处理流程是:先判断入参是否合规,再进行后续的逻辑处理.仅当入参合规时,才进行后续的逻辑处理
	// Tips: 因此我把这部分逻辑判断放在根节点的处理前边
	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

#### 2.3.2 添加对应的测试用例

`router_test.go`:

```go
// TestRouter_AddRoute_Illegal_Case 测试路由注册功能的非法用例
func TestRouter_AddRoute_Illegal_Case(t *testing.T) {
	r := newRouter()
	mockHandleFunc := func(ctx Context) {}

	// step1. 断言路由注册功能的非法用例
	// 1.1 测试路由为空字符串
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "", mockHandleFunc)
	}, "web: 路由不能为空字符串")

	// 1.2 测试路由不以"/"开头
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "login", mockHandleFunc)
	}, "web: 路由必须以 '/' 开头")

	// 1.3 测试路由以"/"结尾
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login/", mockHandleFunc)
	}, "web: 路由不能以 '/' 结尾")
}
```

### 2.4 包含连续的`/`路由

#### 2.4.1 对包含连续的`/`路由进行限制

这里老师的检测方式很巧妙:直接判断按`/`分割`path`后的字符串切片中,是否包含空字符串即可.有空字符串就说明`path`中包含连续的`/`.

TODO:这里这一周的课程结束后要去试一下我想象中的先检测后进逻辑的方案

`router.go`:

```go
package illegalTestCase

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// 1.2 检测路由是否以"/"开头
	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	// 1.3 检测路由是否以"/"结尾
	// Tips: 这个逻辑判断放在根节点的处理后边确实是可以省点代码 但是我认为那样不太好理解
	// Tips: 我认为正常的处理流程是:先判断入参是否合规,再进行后续的逻辑处理.仅当入参合规时,才进行后续的逻辑处理
	// Tips: 因此我把这部分逻辑判断放在根节点的处理前边
	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 若切割后的路由段为空字符串,则说明路由中有连续的"/"
		if segment == "" {
			panic("web: 路由中不得包含连续的'/'")
		}
		
		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

#### 2.4.2 添加对应的测试用例

`router_test.go`:

```go
// TestRouter_AddRoute_Illegal_Case 测试路由注册功能的非法用例
func TestRouter_AddRoute_Illegal_Case(t *testing.T) {
	r := newRouter()
	mockHandleFunc := func(ctx Context) {}

	// step1. 断言路由注册功能的非法用例
	// 1.1 测试路由为空字符串
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "", mockHandleFunc)
	}, "web: 路由不能为空字符串")

	// 1.2 测试路由不以"/"开头
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "login", mockHandleFunc)
	}, "web: 路由必须以 '/' 开头")

	// 1.3 测试路由以"/"结尾
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login/", mockHandleFunc)
	}, "web: 路由不能以 '/' 结尾")

	// 1.4 测试路由中包含连续的"/"
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login///", mockHandleFunc)
	}, "web: 路由中不得包含连续的'/'")
}
```

### 2.5 路由的重复注册问题

路由重复注册:即对一个已经存在于路由树中的路由进行再次注册.

这里我们的设计是:**不允许使用者注册一个已经存在于路由树中的路由**

判断的依据也很简单:找到目标节点之后,检测该节点是否存在HandleFunc即可.

#### 2.5.1 根节点的重复注册问题

**a. 测试用例**

`router_test.go`:

```go
// TestRouter_AddRoute_Illegal_Case 测试路由注册功能的非法用例
func TestRouter_AddRoute_Illegal_Case(t *testing.T) {
	r := newRouter()
	mockHandleFunc := func(ctx Context) {}
	// 为测试路由冲突 先注册路由
	r.AddRoute(http.MethodGet, "/", mockHandleFunc)

	// step1. 断言路由注册功能的非法用例
	// 1.1 测试路由为空字符串
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "", mockHandleFunc)
	}, "web: 路由不能为空字符串")

	// 1.2 测试路由不以"/"开头
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "login", mockHandleFunc)
	}, "web: 路由必须以 '/' 开头")

	// 1.3 测试路由以"/"结尾
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login/", mockHandleFunc)
	}, "web: 路由不能以 '/' 结尾")

	// 1.4 测试路由中包含连续的"/"
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login///", mockHandleFunc)
	}, "web: 路由中不得包含连续的'/'")

	// 1.5 测试路由重复注册
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/", mockHandleFunc)
	}, "web: 路由冲突,重复注册路由 [/] ")
}
```

**b. 添加对根节点的路由重复注册限制**

`router.go`:

```go
package illegalTestCase

import "strings"

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// 1.2 检测路由是否以"/"开头
	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	// 1.3 检测路由是否以"/"结尾
	// Tips: 这个逻辑判断放在根节点的处理后边确实是可以省点代码 但是我认为那样不太好理解
	// Tips: 我认为正常的处理流程是:先判断入参是否合规,再进行后续的逻辑处理.仅当入参合规时,才进行后续的逻辑处理
	// Tips: 因此我把这部分逻辑判断放在根节点的处理前边
	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		if root.HandleFunc != nil {
			panic("web: 路由冲突,重复注册路由 [/] ")
		}
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 若切割后的路由段为空字符串,则说明路由中有连续的"/"
		if segment == "" {
			panic("web: 路由中不得包含连续的'/'")
		}

		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}
	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

#### 2.5.2 普通节点的重复注册问题

**a. 测试用例**

`router_test.go`:

```go
// TestRouter_AddRoute_Illegal_Case 测试路由注册功能的非法用例
func TestRouter_AddRoute_Illegal_Case(t *testing.T) {
	r := newRouter()
	mockHandleFunc := func(ctx Context) {}
	// 为测试路由冲突 先注册路由
	r.AddRoute(http.MethodGet, "/", mockHandleFunc)
	r.AddRoute(http.MethodGet, "/user", mockHandleFunc)

	// step1. 断言路由注册功能的非法用例
	// 1.1 测试路由为空字符串
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "", mockHandleFunc)
	}, "web: 路由不能为空字符串")

	// 1.2 测试路由不以"/"开头
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "login", mockHandleFunc)
	}, "web: 路由必须以 '/' 开头")

	// 1.3 测试路由以"/"结尾
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login/", mockHandleFunc)
	}, "web: 路由不能以 '/' 结尾")

	// 1.4 测试路由中包含连续的"/"
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/login///", mockHandleFunc)
	}, "web: 路由中不得包含连续的'/'")

	// 1.5 测试路由重复注册
	// a. 根节点重复注册
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/", mockHandleFunc)
	}, "web: 路由冲突,重复注册路由 [/] ")

	// b. 普通节点重复注册
	assert.Panicsf(t, func() {
		r.AddRoute(http.MethodGet, "/user", mockHandleFunc)
	}, "web: 路由冲突,重复注册路由 [/user] ")
}
```

**b. 添加对普通节点的路由重复注册限制**

`router.go`:

```go
package illegalTestCase

import (
	"fmt"
	"strings"
)

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) AddRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// 1.2 检测路由是否以"/"开头
	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	// 1.3 检测路由是否以"/"结尾
	// Tips: 这个逻辑判断放在根节点的处理后边确实是可以省点代码 但是我认为那样不太好理解
	// Tips: 我认为正常的处理流程是:先判断入参是否合规,再进行后续的逻辑处理.仅当入参合规时,才进行后续的逻辑处理
	// Tips: 因此我把这部分逻辑判断放在根节点的处理前边
	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		// 判断根节点是否路由冲突
		if root.HandleFunc != nil {
			panic("web: 路由冲突,重复注册路由 [/] ")
		}
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 若切割后的路由段为空字符串,则说明路由中有连续的"/"
		if segment == "" {
			panic("web: 路由中不得包含连续的'/'")
		}

		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}

	// 判断普通节点是否路由冲突
	if target.HandleFunc != nil {
		panic(fmt.Sprintf("web: 路由冲突,重复注册路由 [%s] ", path))
	}

	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

## PART3. 问题:`AddRoute()`方法的`method`参数需要校验吗?

这个其实不需要校验,**因为调用`AddRoute()`方法的代码都是咱自己写的代码.而非框架使用者写的**.

为了确保这一点,需要将`AddRoute()`方法改为`addRoute()`方法.

`router.go`:

```go
package illegalTestCase

import (
	"fmt"
	"strings"
)

// router 路由森林 用于支持对路由树的操作
type router struct {
	// trees 路由森林 按HTTP动词组织路由树
	// 该map中 key为HTTP动词 value为路由树的根节点
	// 即: 每个HTTP动词对应一棵路由树 指向每棵路由树的根节点
	trees map[string]*node
}

// newRouter 创建路由森林
func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
func (r *router) addRoute(method string, path string, handleFunc HandleFunc) {
	// step1. 检测路由是否合规

	// 1.1 检测路由是否为空字符串
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	// 1.2 检测路由是否以"/"开头
	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	// 1.3 检测路由是否以"/"结尾
	// Tips: 这个逻辑判断放在根节点的处理后边确实是可以省点代码 但是我认为那样不太好理解
	// Tips: 我认为正常的处理流程是:先判断入参是否合规,再进行后续的逻辑处理.仅当入参合规时,才进行后续的逻辑处理
	// Tips: 因此我把这部分逻辑判断放在根节点的处理前边
	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	// step2. 找到路由树
	root, ok := r.trees[method]
	// 如果没有找到路由树,则创建一棵路由树
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step3. 判断path是否为根节点 如果是则直接设置HandleFunc并返回即可
	if path == "/" {
		// 判断根节点是否路由冲突
		if root.HandleFunc != nil {
			panic("web: 路由冲突,重复注册路由 [/] ")
		}
		root.HandleFunc = handleFunc
		return
	}

	// step4. 切割path
	// Tips: 去掉前导的"/" 否则直接切割出来的第一个元素为空字符串
	// Tips: 以下代码是老师写的去掉前导的"/"的方式 我认为表达力有点弱 但是性能应该会好于strings.TrimLeft
	// Tips: 以下代码会有问题,因为假如前导字符不是"/" 则不该被去掉
	// path = path[1:]
	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	// step3. 为路由树添加路由
	// Tips: 此处我认为用target指代要添加路由的节点更好理解
	target := root
	for _, segment := range segments {
		// 若切割后的路由段为空字符串,则说明路由中有连续的"/"
		if segment == "" {
			panic("web: 路由中不得包含连续的'/'")
		}

		// 如果路由树中途有节点没有创建,则创建该节点;
		// 如果路由树中途存在子节点,则找到该子节点
		child := target.childOrCreate(segment)
		// 继续为子节点创建子节点
		target = child
	}

	// 判断普通节点是否路由冲突
	if target.HandleFunc != nil {
		panic(fmt.Sprintf("web: 路由冲突,重复注册路由 [%s] ", path))
	}

	// 为目标节点设置HandleFunc
	target.HandleFunc = handleFunc
}
```

也将`Server`接口的注册路由方法改为私有即可:

`serverInterface.go`:

```go
package illegalTestCase

import "net/http"

// Server WEB服务器接口
type Server interface {
	// Handler 组合http.Handler接口
	http.Handler

	// Start 启动WEB服务器
	Start(addr string) error

	// addRoute 注册路由
	addRoute(method string, path string, handleFunc HandleFunc)
}
```

最后再改所有调用了`AddRoute()`的地方就可以了(`httpServer.go`和`router_test.go`,此处就不贴代码了,自己`command + r`一下就行了).

## PART4. 问题:`addRoute()`方法的`handleFunc`参数需要校验吗?

不需要.因为正常的使用者都不会传个nil进来的.按照我们现在的设计,他传个nil进来,就相当于没有注册这个路由.再者来讲,从使用者的视角上来看,`path`字段确实容易写错,但是`handleFunc`基本上正常使用的情况下都不会写错的.因此没必要校验.


# 3.05 路由树-静态匹配之路由查找

## PART1. 定义查找路由的方法

所谓查找路由,就是根据给定的HTTP动词和uri,在路由树中查找对应的节点.

`router.go`:

```go
// findRoute 根据给定的HTTP方法和路由路径,在路由森林中查找对应的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
	// 沿着树深度遍历
}
```

注:此处也没有把全部`router.go`的代码放进来,因为太乱了,只放了和本小节有关的部分

## PART2. 定义测试用例

### 2.1 在测试函数中注册路由

这一部分和注册路由的测试用例代码完全相同

`router_test.go`:

```go
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用addRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestRouter_findRoute 测试路由查找功能
func TestRouter_findRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}

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

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

### 2.2 构造测试用例

#### 2.2.1 定义测试用例的类型

这里我们需要通过这个类型知道如下信息:

* 在给定的HTTP动词和uri的前提下,是否在路由树中找到了节点?
* 在给定的HTTP动词和uri的前提下,找到的节点和预定义的节点是否相同?

`router_test.go`:

```go
// TestCaseNode 测试用例
type TestCaseNode struct {
	// name 子测试用例的名称
	name string
	// method HTTP动词
	method string
	// path 路由路径
	path string
	// isFound 是否找到路由
	isFound bool
	// wantNode 期望的路由节点
	wantNode *node
}
```

#### 2.2.2 定义测试的过程

* step1. 判断是否在路由树中找到节点
* step2. 判断找到的节点和预定义的节点是否相同
  * 这里需要注意的是,和之前写`addRoute()`方法的测试用例相同,不能用`assert.Equal()`方法直接比对两个`node`结构体的实例,因为`HandleFunc`不可比

`router_test.go`:

```go
// TestNode 测试路由树节点
// 由于此处我们要测试的是路由树的结构,因此不需要在测试路由树节点中添加路由处理函数
// 调用addRoute时写死一个HandleFunc即可
type TestNode struct {
	method string
	path   string
}

// TestCaseNode 测试用例
type TestCaseNode struct {
	// name 子测试用例的名称
	name string
	// method HTTP动词
	method string
	// path 路由路径
	path string
	// isFound 是否找到路由
	isFound bool
	// wantNode 期望的路由节点
	wantNode *node
}

// TestRouter_findRoute 测试路由查找功能
func TestRouter_findRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}

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

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

	// step2. 构造测试用例
	testCases := []TestCaseNode{}

	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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

#### 2.2.3 构造测试用例

所谓构造测试用例,就是要考虑`findRoute()`方法会遇到什么场景(或者也可以说遇到什么边缘条件):

* HTTP动词不存在
* 完全命中
* 命中了但找到的node中,handler是nil
* 根节点
* 没有找到path

## PART3. 以TDD的方式开发`findRoute()`方法

### 3.1 HTTP动词不存在的情况

#### 3.1.1 实现

这个case比较简单,当给定的HTTP动词不存在时,直接返回`nil, false`即可:

`router.go`:

```go
// findRoute 根据给定的HTTP方法和路由路径,在路由森林中查找对应的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
	_, ok := r.trees[method]
	// 给定的HTTP动词在路由森林中不存在对应的路由树,则直接返回false
	if !ok {
		return nil, false
	}

	panic("implement me")
}
```

#### 3.1.2 测试

`router_test.go`:

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

	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
		wantNode *node
	}{
		// 测试HTTP动词不存在的用例
		{
			name:     "method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: 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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

注:这里把`testCases`的类型改成了匿名结构体,不然没法用IDE的单个测试功能.单个测试功能如下图示:

![单个测试功能](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-a6b7c4d3232c47937e23b7cdacf50d744a941452%2F%E5%8D%95%E4%B8%AA%E6%B5%8B%E8%AF%95%E5%8A%9F%E8%83%BD.png?alt=media)

### 3.2 完全命中的情况

#### 3.2.1 实现

* step1. 切割path
  * 切割`path`时注意前导`/`和后置`/`
* step2. 从根节点开始按"层次"(切割`path`后的字符串切片其实每个元素就是"一层")找子节点,找到了则继续深入一层
* step3. 没找到就返回`nil, false`即可

`router.go`:

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

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

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

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

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

`node.go`:(此处只写新增的方法)

```go
// childOf 根据给定的path在当前节点的子节点映射中查找对应的子节点
func (n *node) childOf(path string) (child *node, found bool) {
	if n.children == nil {
		return nil, false
	}
	child, found = n.children[path]
	return child, found
}
```

#### 3.2.2 测试

`router_test.go`:

```go
// TestRouter_findRoute 测试路由查找功能
func TestRouter_findRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// GET方法路由树
		TestNode{
			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
		wantNode *node
	}{
		// 测试HTTP动词不存在的用例
		{
			name:     "method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},

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

	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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

### 3.3 命中了,但找到的node中,handler是nil的情况

这个case只需要加一个测试用例即可,因为按照我们设计的`findRoute()`,在返回时并没有关注`HandleFunc`是否为nil

`router_test.go`:

```go
// TestRouter_findRoute 测试路由查找功能
func TestRouter_findRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// GET方法路由树
		TestNode{
			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
		wantNode *node
	}{
		// 测试HTTP动词不存在的用例
		{
			name:     "method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},

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

		// 测试命中了节点但节点的HandleFunc为nil的情况
		{
			name:    "order",
			method:  http.MethodGet,
			path:    "/order",
			isFound: true,
			wantNode: &node{
				path: "order",
				children: map[string]*node{
					"detail": &node{
						path:       "detail",
						children:   nil,
						HandleFunc: mockHandleFunc,
					},
				},
				HandleFunc: 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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

### 3.4 根节点

#### 3.4.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
		wantNode *node
	}{
		// 测试HTTP动词不存在的用例
		{
			name:     "method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},

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

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

		// 测试根节点
		{
			name:    "",
			method:  http.MethodGet,
			path:    "/",
			isFound: true,
			wantNode: &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,
			},
		},
	}

	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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

![没有通过的单测](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-30974ab134d322d6a99b48a1bc7a74f2607a4a7e%2F%E6%B2%A1%E6%9C%89%E9%80%9A%E8%BF%87%E7%9A%84%E5%8D%95%E6%B5%8B.png?alt=media)

#### 3.4.2 修bug

![断点调试](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3f53019f49b68792c7554f3b9c13fc4f083ad922%2F%E6%96%AD%E7%82%B9%E8%B0%83%E8%AF%95.png?alt=media)

![步进调试](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-da5ba99c335b648ceff1553c01bff94c82658762%2F%E6%AD%A5%E8%BF%9B%E8%B0%83%E8%AF%95.png?alt=media)

![步进调试-定位问题](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-28b01ceeaadaea80e97f63f1cc7243ae21281f9b%2F%E6%AD%A5%E8%BF%9B%E8%B0%83%E8%AF%95-%E5%AE%9A%E4%BD%8D%E9%97%AE%E9%A2%98.png?alt=media)

可以看到,过滤掉`/`之后,切割`path`的结果不符合预期.

修复思路:对根节点做特殊处理

`router.go`:

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

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

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

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

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

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

### 3.5 没有找到path

这里也是先加测试用例,发现能通过,就可以了.

`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
		wantNode *node
	}{
		// 测试HTTP动词不存在的用例
		{
			name:     "method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},

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

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

		// 测试根节点
		{
			name:    "",
			method:  http.MethodGet,
			path:    "/",
			isFound: true,
			wantNode: &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,
			wantNode: 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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

## 附录

TODO:要去看v2代码中的用例,然后再拿回来跑


# 3.06 路由树-静态匹配之集成Server

本节课工程结构如下:

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

0 directories, 8 files
```

## PART1. 实现`HTTPServer.serve()`方法

之前的课程中说过,该方法的职责为:**查找路由树并执行命中的业务逻辑**.我们也已经实现了`router.findRoute()`.

`httpServer.go`:此处只写做了修改的方法,其他没有改动的方法就不贴了,看着太乱

* 实现`httpServer.serve()`方法:

```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.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		// 此处确实会报错 但是作为一个WEB框架 遇上了这种错误也没有特别好的处理办法
		// 最多只能是落个日志
		_, _ = ctx.Resp.Write([]byte("Not Found"))
		return
	}

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

* 删除`http. ServeHTTP()`方法中的`panic()`

```go
// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 查找路由树并执行命中的业务逻辑
	s.serve(ctx)
}
```

## PART2. 修改`HandleFunc`类型的入参

### 2.1 修改入参类型为`*Context`

之前入参类型为`Context`

`handleFunc.go`:

```go
type HandleFunc func(ctx *Context)
```

### 2.2 修改其他受影响的地方

此处只列出受影响的方法或文件(文件基本上都是测试文件),读者自行比对修改即可

* `HTTPServer.serve()`
* `httpServer_test.go`
* `router_test.go`

## PART3. 测试

TODO:文件开头的那句`go:build e2e`的含义要去查

在`httpServer_test.go`新创建一个测试函数:

`httpServer_test.go`:

```go
func TestServer_serve(t *testing.T) {
	s := NewHTTPServer()
	handleFunc := func(ctx *Context) {
		// 直接调用http.ResponseWriter的Write方法时 默认响应码为200
		ctx.Resp.Write([]byte("hello order detail"))
	}
	s.addRoute(http.MethodGet, "/order/detail", handleFunc)
	_ = s.Start(":8081")
}
```

其实这里还有一个问题:如果将`s := NewHTTPServer()`更改为`s := &HTTPServer{}`,就会因为`router`为空而触发panic.

![IDE调试功能](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-252af706fceb802ae177e7c1a9ff9017596bed8c%2FIDE%E8%B0%83%E8%AF%95%E5%8A%9F%E8%83%BD.png?alt=media)

## PART4. 修改`HTTPServer`的成员属性

将`*router`修改为`router`

修改原因:就是为了和PPT上的统一一下.这里我看过,GIN的`Engine`是组合了`RouterGroup`;而Beego的`HttpServer`则是组合了`*ControllerRegister`.倒不是大家都用非指针类型

* `httpServer.go`:

```go
// HTTPServer HTTP服务器
type HTTPServer struct {
	router
}
```

* `router.go`:

```go
// newRouter 创建路由森林
func newRouter() router {
	return router{
		trees: map[string]*node{},
	}
}
```

* `router_test.go`中也有修改

至此,静态路由匹配功能完成.将此版本作为v2.

## 附录

### 需要课后尝试的内容

将`HttpServer`结构体修改为私有

这个修改的目的在于:强迫使用者必须使用`NewHttpServer()`函数来创建`HttpServer`结构体的实例.否则他自己直接`s := &HTTPServer{}`触发panic.

### 何时将成员属性类型定义为指针类型?何时将成员属性类型定义为结构体类型?

以HTTPServer为例:

```go
type HTTPServer struct {
	*router
}
```

#### case1. 使用者用`&HTTPServer`的情况

我们期望使用者用`HTTPServer`的指针,因此其成员属性`router`是不是指针就不重要了,都行

这里我试过:

```go
type HTTPServer struct {
	*router
}

s := &HTTPServer{}
if s.router == nil {
	fmt.Printf("s.router is nil\n")
}
```

则打印:`s.router is nil`

但如果改为:

```
type HTTPServer struct {
	router
}

s := &HTTPServer{}
fmt.Printf("%p\n", &s.router)
```

则打印:`0xc00011a050`.可以看到已经给`router`分配了内存

#### case2. 使用者用`HTTPServer`的情况

```go
type HTTPServer struct {
	*router
}

s := HTTPServer{}
if s.router == nil {
	fmt.Printf("s.router is nil\n")
}
```

则打印:`s.router is nil`

```go
type HTTPServer struct {
	router
}

s := HTTPServer{}
fmt.Printf("%p\n", &s.router)
```

则打印:`0xc0000aa050`.同样已经给`router`分配了内存.但是!这种方式要考虑值传递的问题.不要忘记GO语言中所有的传递都是值传递!

结论:**拿不准就用指针!**


# 3.07 路由树-通配符匹配之路由注册

本节课工程结构如下:

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

0 directories, 8 files
```

## PART1. 学习路线

![学习路线](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-9181d753fdf3c7db33e5b66260531391169b8905%2F%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF.png?alt=media)

## PART2. 通配符匹配的定义与设计

### 2.1 通配符匹配的定义

通配符匹配:是指用`*`表达匹配任何路径

### 2.2 通配符匹配的设计

这里就要考虑定义中所说的**任何路径**.

#### Question1:若用户输入的URI为`/a/b/c`,能否命中`/a/*`?

这个问题本质上是我们作为设计者,要考虑**通配符`*`在路由中的语义,是只能代表1段路由,还是可以代表多段路由?**

这里我们选择**让通配符`*`只能表达1段路由**.

我们假定通配符`*`表达多段路由.那么从框架使用者的视角上来看,路由`/a/*`和`/a/*/*`就很让人困惑:当路由为`/a/b/c`时,究竟匹配到了哪个路由?

从框架设计者的视角上来看,就干脆不要给使用者做出这种不良实践的机会.

#### Question2:若注册了2个路由:`/user/123/home`和`/user/*/*`,那么用户输入路径`/user/123/detail`时,能否命中路由`/user/*/*`?

这个问题本质上是我们作为设计者,要考虑我们的**路由匹配过程是否可回溯?**

可回溯的匹配过程如下图示:

![可回溯的路由匹配过程](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-df6cf399b13c3db5a27a41bb2ab11926a34139a5%2F%E5%8F%AF%E5%9B%9E%E6%BA%AF%E7%9A%84%E8%B7%AF%E7%94%B1%E5%8C%B9%E9%85%8D%E8%BF%87%E7%A8%8B.jpg?alt=media)

简单理解就是:当发现`/user/123/home`不能匹配`/user/123/detail`时,要回溯到`/user/*`节点进行进一步查找的过程.

可以做,但这是一个**投入大产出低的特性**.

这里我们选择不支持这种路由,有2个原因:

1. 可回溯的特性会使得你的代码变得非常复杂
2. 从框架设计者的角度上来讲,通过不提供这种匹配方式的做法,限制框架的使用者不能定义形如`/user/*/*`的路由.
   * `/user/*/*`不好的地方在于:当使用者A读使用者B的代码时,他一定是要非常清晰的了解一个前提:**当路由`/user/123/detail`不能匹配到`/user/123/home`时,则能够匹配到`/user/*/*`**.这样的代码可读性本身就不高,因此通过不提供支持回溯匹配过程的方式,限制使用者不要去写这种模糊的代码
   * 更进一步的,框架的使用者本身也不应该同时注册`/user/123/home`和`/user/*/*`这两个路由

## PART3. 实现通配符节点的创建

### 3.1 修改`Node`的结构

`node.go`:

```go
// node 路由树的节点
type node struct {
	// path 当前节点的路径
	path string

	// children 子路由路径到子节点的映射
	children map[string]*node
	
	// wildcardChild 通配符子节点
	wildcardChild *node

	// HandleFunc 路由对应的业务逻辑
	HandleFunc
}
```

由于通配符节点的逻辑是需要特殊处理的(不管在注册路由还是查找路由时),因此需要为`node`结构体单独定义表示其通配符子节点的成员属性.

### 3.2 定义测试用例

此处还是在`router_test.go`中新建一个函数用于测试通配符匹配:

```go
// TestRouter_wildcard 测试通配符路由的注册与查找
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: {
				path: "/",
				children: map[string]*node{
					"order": {
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: nil,
				HandleFunc:    nil,
			},
		},
	}

	msg, ok := wantRouter.equal(&r)
	assert.True(t, ok, msg)
}
```

现在运行这个测试用例,结果是不符合预期的:

```
/usr/local/go/bin/go tool test2json -t /private/var/folders/9x/kkdcw3dx7js8frvbxh8gns580000gn/T/dlvLauncher.sh /Users/yanglei/Applications/GoLand.app/Contents/plugins/go-plugin/lib/dlv/mac/dlv --listen=127.0.0.1:57140 --headless=true --api-version=2 --check-go-version=false --only-same-user=false exec /Users/yanglei/Library/Caches/JetBrains/GoLand2023.2/tmp/GoLand/___TestRouter_wildcard_in_server_12_matchWildcard.test -- -test.v -test.paniconexit0 -test.run ^\QTestRouter_wildcard\E$
=== RUN   TestRouter_wildcard
    router_test.go:426: 
        	Error Trace:	/Users/yanglei/Desktop/GO实战训练营/GoInAction/code/week1/server/12-matchWildcard/router_test.go:426
        	Error:      	Should be true
        	Test:       	TestRouter_wildcard
        	Messages:   	GET-两个节点的子节点数量不相等,源节点的子节点数量为 0,目标节点的子节点数量为 1
--- FAIL: TestRouter_wildcard (0.00s)

FAIL
```

很明显,按我们现在的逻辑,`/order/*`中,通配符`*`成为了`order`节点的子节点,而我们预期中通配符`*`应该是`order`节点的通配符子节点(也就是`wildcardChild`字段值).

![预期结果](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-b893949e74f8a410e30b3a71bc828aea518ac9fc%2F%E9%A2%84%E6%9C%9F%E7%BB%93%E6%9E%9C.png?alt=media)

![实际结果](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-52ae3898c2a1831493fe1b5ed43b0a850f8ef090%2F%E5%AE%9E%E9%99%85%E7%BB%93%E6%9E%9C.png?alt=media)

注:这里还是看他TDD的思路,是用测试用例告诉自己"哪里不符合预期".

很明显,在创建子节点的操作中出问题了.创建时不应该把`*`放在子节点集合里,而是应该放在`wildcardChild`字段上.

### 3.3 修改创建子节点的逻辑

`node.go`:

```go
// childOrCreate 本方法用于在节点上获取给定的子节点,如果给定的子节点不存在则创建
func (n *node) childOrCreate(segment string) *node {
	// 若路径为通配符 则查找当前节点的通配符子节点 或创建一个当前节点的通配符子节点 并返回
	if segment == "*" {
		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
}
```

`childOrCreate()`方法中,添加了单独处理通配符子节点的逻辑.注意在`childOrCreate()`方法并不需要单独检测通配符子节点是否重复注册,因为其调用者`router.addRoute()`方法中有这个逻辑,不需要也不应该在`childOrCreate()`方法中检测通配符子节点是否重复注册.

这时候再去跑测试用例,实际上就已经通过了.

### 3.4 修改判断子节点相等的逻辑

在比对完两个节点的子节点映射数量之后,还要比对两个节点的通配符子节点是否相同.

`router_test.go`:

```go
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(target *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if target == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != target.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, target.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(target.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}

	// 若两个节点的通配符子节点不相等 则不相等
	if n.wildcardChild != nil {
		if target.wildcardChild == nil {
			return fmt.Sprintf("目标节点的通配符子节点为空"), false
		}
		_, wildcardIsEqual := n.wildcardChild.equal(target.wildcardChild)
		if !wildcardIsEqual {
			return fmt.Sprintf("两个节点的通配符子节点不相等"), false
		}
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(target.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := target.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

添加了比对两个节点的通配符子节点是否相等的逻辑

注:这里我将原来的形参`y`改名为`target`,为了提升可读性.

再跑单测也还是能跑的通的.

### 3.5 添加其他测试用例

#### 3.5.1 根节点的通配符子节点

因为我们的实现中针对根节点有特殊逻辑,所以这里我们单独测试根节点的通配符子节点

`router_test.go`:

```go
// TestRouter_wildcard 测试通配符路由的注册与查找
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: {
				path: "/",
				children: map[string]*node{
					"order": {
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path:          "*",
					children:      nil,
					wildcardChild: nil,
					HandleFunc:    mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(&r)
	assert.True(t, ok, msg)
}
```

跑单测能够顺利通过.

#### 3.5.2 通配符子节点的通配符子节点

`router_test.go`:

```go
// TestRouter_wildcard 测试通配符路由的注册与查找
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*",
		},
		// 通配符子节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*/*",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: {
				path: "/",
				children: map[string]*node{
					"order": {
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path:     "*",
					children: nil,
					wildcardChild: &node{
						path:          "*",
						children:      nil,
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(&r)
	assert.True(t, ok, msg)
}
```

#### 3.5.3 通配符子节点的普通子节点

`router_test.go`:

```go
// TestRouter_wildcard 测试通配符路由的注册与查找
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*",
		},
		// 通配符子节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*/*",
		},
		// 通配符子节点的普通子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*/*/order",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: {
				path: "/",
				children: map[string]*node{
					"order": {
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path:     "*",
					children: nil,
					wildcardChild: &node{
						path: "*",
						children: map[string]*node{
							"order": {
								path:          "order",
								children:      nil,
								wildcardChild: nil,
								HandleFunc:    mockHandleFunc,
							},
						},
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(&r)
	assert.True(t, ok, msg)
}
```

#### 3.5.4 通配符子节点的普通子节点的通配符子节点

`router_test.go`:

```go
// TestRouter_wildcard 测试通配符路由的注册与查找
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*",
		},
		// 通配符子节点的通配符子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*/*",
		},
		// 通配符子节点的普通子节点测试用例
		{
			method: http.MethodGet,
			path:   "/*/*/order",
		},
		// 通配符子节点的普通子节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*/*/order/*",
		},
	}

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

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

	// step2. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: {
				path: "/",
				children: map[string]*node{
					"order": {
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path:     "*",
					children: nil,
					wildcardChild: &node{
						path: "*",
						children: map[string]*node{
							"order": {
								path:     "order",
								children: nil,
								wildcardChild: &node{
									path:          "*",
									children:      nil,
									wildcardChild: nil,
									HandleFunc:    mockHandleFunc,
								},
								HandleFunc: mockHandleFunc,
							},
						},
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(&r)
	assert.True(t, ok, msg)
}
```


# 3.08 路由树-通配符匹配之路由查找与测试

本节课工程结构如下:

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

0 directories, 8 files
```

## PART1. 修改`childOf()`方法

之前的`childOf()`方法是仅在当前节点的子节点映射中查找子节点,但很明显现在需要再查找当前节点的通配符子节点.

`node.go`:

```go
// childOf 根据给定的path在当前节点的子节点映射中查找对应的子节点(即匹配到了静态路由)
// 若未在子节点映射中找到对应子节点 则尝试返回当前节点的通配符子节点
func (n *node) childOf(path string) (child *node, found bool) {
	// 当前节点的子节点映射为空 则有可能匹配到通配符节点
	if n.children == nil {
		return n.wildcardChild, n.wildcardChild != nil
	}

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

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

## PART2. 编写测试用例

这里还是在`router_test.go`新建一个单独测试查找通配符路由的函数.

### 2.1 测试普通节点的通配符子节点

`router_test.go`:

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

	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
		wantNode *node
	}{
		// 普通节点的通配符子节点测试用例
		{
			name:    "order wildcard",
			method:  http.MethodGet,
			path:    "/order/abc",
			isFound: true,
			wantNode: &node{
				path:          "*",
				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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

测试顺利通过

### 2.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
		wantNode *node
	}{
		// 普通节点的通配符子节点测试用例
		{
			name:    "order wildcard",
			method:  http.MethodGet,
			path:    "/order/abc",
			isFound: true,
			wantNode: &node{
				path:          "*",
				children:      nil,
				wildcardChild: nil,
				HandleFunc:    mockHandleFunc,
			},
		},
		// 普通节点下普通子节点和通配符子节点共存的测试用例
		{
			name:    "order detail",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			wantNode: &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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

测试顺利通过

## PART3. 测试Server的通配符匹配

`httpServer_test.go`:

```go
func TestServer_serve(t *testing.T) {
	s := NewHTTPServer()
	handleFunc := func(ctx *Context) {
		// 直接调用http.ResponseWriter的Write方法时 默认响应码为200
		ctx.Resp.Write([]byte("hello order detail"))
	}
	s.GET("/order/detail", handleFunc)

	wildcardHandleFunc := func(ctx *Context) {
		respPath := ""

		pathSegments := strings.Split(ctx.Req.URL.Path, "/")
		for _, pathSegment := range pathSegments {
			if pathSegment == "" {
				continue
			}
			respPath += pathSegment
			respPath += " "
		}

		respPath = strings.TrimRight(respPath, " ")
		respMsg := fmt.Sprintf("hello %s", respPath)

		ctx.Resp.Write([]byte(respMsg))
	}
	s.GET("/order/*", wildcardHandleFunc)

	_ = s.Start(":8081")
}
```


# 3.09 路由树-参数路径之基本注册和查找

本节课工程结构如下:

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

0 directories, 8 files
```

## PART1. 学习路线

![学习路线](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-1086b25fe878e14e1bcdff6563e674a8eb5ca00b%2F%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF.png?alt=media)

## PART2. 参数路径的定义与设计

### 2.1 参数路径的定义

参数路径:就是指在路径中带上参数,同时这些参数对应的值可以被业务取出来使用.在我们的设计中用`:参数名`的形式表示路由参数

例:`/user/:id`,如果输入路径`/user/123`,则会命中这个路由`/user/:id`,并且在业务函数中可以取到变量`id = 123`.

### 2.2 参数路径的设计

那么问题来了:

* **是否允许同样的参数路径和通配符匹配一起注册?**
  * 例如同时注册`/user/*`和`/user/:id`

**可以,但没必要,用户也不应该设计这种路由**

## PART3. 实现参数路由节点的创建

### 3.1 修改`node`的结构

和实现通配符路由注册的过程相同.由于参数路由节点的逻辑是需要特殊处理的(不管在注册路由还是查找路由时),因此需要为`node`结构体单独定义表示其参数路由子节点的成员属性.

`node.go`:

```go
// node 路由树的节点
type node struct {
	// path 当前节点的路径
	path string

	// children 子路由路径到子节点的映射
	children map[string]*node

	// wildcardChild 通配符子节点
	wildcardChild *node
	
	// paramChild 参数子节点
	paramChild *node

	// HandleFunc 路由对应的业务逻辑
	HandleFunc
}
```

### 3.2 定义测试用例

此处还是在`router_test.go`中新建一个函数用于测试参数路由匹配:

`router_test.go`:

```go
// TestRouter_addParamRoute 测试注册参数路由的结果是否符合预期
func TestRouter_addParamRoute(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. 验证路由树 断言二者是否相等
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: {
				path: "/",
				children: map[string]*node{
					"order": {
						path: "order",
						children: map[string]*node{
							"detail": {
								path:          "detail",
								children:      nil,
								wildcardChild: nil,
								paramChild: &node{
									path:          ":id",
									children:      nil,
									wildcardChild: nil,
									paramChild:    nil,
									HandleFunc:    mockHandleFunc,
								},
								HandleFunc: nil,
							},
						},
						wildcardChild: nil,
						paramChild:    nil,
						HandleFunc:    nil,
					},
				},
				wildcardChild: nil,
				paramChild:    nil,
				HandleFunc:    nil,
			},
		},
	}

	msg, ok := wantRouter.equal(&r)
	assert.True(t, ok, msg)
}
```

这个时候测试肯定是不通过的.我们断点调试看一下预期和现状的差异:

![预期结果](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-4a43d7d1a29eae7d808a4bb48b1519705432c0ae%2F%E9%A2%84%E6%9C%9F%E7%BB%93%E6%9E%9C.png?alt=media)

![实际结果](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d6cdb21688c97ee9fd7c0e46e5c00c4876b49d1c%2F%E5%AE%9E%E9%99%85%E7%BB%93%E6%9E%9C.png?alt=media)

和实现通配符路由注册的过程相同,先调整创建节点的逻辑

### 3.3 修改创建子节点的逻辑

`node.go`:

```go
// childOrCreate 本方法用于在节点上获取给定的子节点,如果给定的子节点不存在则创建
func (n *node) childOrCreate(segment string) *node {
	// 如果路径为参数 则查找当前节点的参数子节点 或创建一个当前节点的参数子节点 并返回
	if strings.HasPrefix(segment, ":") {
		if n.paramChild == nil {
			n.paramChild = &node{
				path: segment,
			}
		}
		return n.paramChild
	}

	// 若路径为通配符 则查找当前节点的通配符子节点 或创建一个当前节点的通配符子节点 并返回
	if segment == "*" {
		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
}

// childOf 根据给定的path在当前节点的子节点映射中查找对应的子节点(即匹配到了静态路由)
// 若未在子节点映射中找到对应子节点 则尝试返回当前节点的通配符子节点
func (n *node) childOf(path string) (child *node, found bool) {
	// 当前节点的子节点映射为空 则有可能匹配到通配符节点
	if n.children == nil {
		return n.wildcardChild, n.wildcardChild != nil
	}

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

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

### 3.4 修改判断子节点相等的逻辑

在比对完两个节点的子节点映射数量之后,还要比对两个节点的参数子节点是否相同.

`router_test.go`:

```go
// equal 比较两棵路由树是否相等
// msg: 两棵路由树不相等时的错误信息
// ok: 两棵路由树是否相等
func (n *node) equal(target *node) (msg string, ok bool) {
	// 如果目标节点为nil 则不相等
	if target == nil {
		return fmt.Sprintf("目标节点为nil"), false
	}

	// 如果两个节点的path不相等 则不相等
	if n.path != target.path {
		return fmt.Sprintf("两个节点的path不相等,源节点的path为 %s,目标节点的path为 %s", n.path, target.path), false
	}

	// 若两个节点的子节点数量不相等 则不相等
	nChildrenNum := len(n.children)
	yChildrenNum := len(target.children)
	if nChildrenNum != yChildrenNum {
		return fmt.Sprintf("两个节点的子节点数量不相等,源节点的子节点数量为 %d,目标节点的子节点数量为 %d", nChildrenNum, yChildrenNum), false
	}
	
	// 若两个节点的参数子节点不相等 则不相等
	if n.paramChild != nil {
		if target.paramChild == nil {
			return fmt.Sprintf("目标节点的参数子节点为空"), false
		}
		_, paramIsEqual := n.paramChild.equal(target.paramChild)
		if !paramIsEqual {
			return fmt.Sprintf("两个节点的参数子节点不相等"), false
		}
	}

	// 若两个节点的通配符子节点不相等 则不相等
	if n.wildcardChild != nil {
		if target.wildcardChild == nil {
			return fmt.Sprintf("目标节点的通配符子节点为空"), false
		}
		_, wildcardIsEqual := n.wildcardChild.equal(target.wildcardChild)
		if !wildcardIsEqual {
			return fmt.Sprintf("两个节点的通配符子节点不相等"), false
		}
	}

	// 若两个节点的handleFunc类型不同 则不相等
	nHandler := reflect.ValueOf(n.HandleFunc)
	yHandler := reflect.ValueOf(target.HandleFunc)
	if nHandler != yHandler {
		return fmt.Sprintf("%s节点的handleFunc不相等,源节点的handleFunc为 %v,目标节点的handleFunc为 %v", n.path, nHandler.Type().String(), yHandler.Type().String()), false
	}

	// 比对两个节点的子节点映射是否相等
	for path, child := range n.children {
		dstChild, ok := target.children[path]
		// 如果源节点的子节点中 存在目标节点没有的子节点 则不相等
		if !ok {
			return fmt.Sprintf("目标节点的子节点中没有path为 %s 的子节点", path), false
		}

		// 比对两个子节点是否相等
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此时再运行测试用例,即可顺利通过

## PART4. 实现参数路由节点的查找

### 4.1 修改`childOf()`方法

之前的`childOf()`方法是先在当前节点的子节点映射中查找子节点,若未找到则尝试返回当前节点的通配符子节点.

而我们需要的结果是:

* 先在当前节点的子节点映射中查找子节点
* 若未找到则尝试返回当前节点的参数路由子节点
* 若还未找到则尝试返回当前节点的通配符子节点

换言之,我们设计的路由优先级为:**静态路由 > 参数路由 > 通配符路由**

`node.go`:

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

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

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

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

**实际上通配符路由不应该和参数路由共存,只是本节课还没实现这个逻辑**.

### 4.2 编写测试用例

`router_test.go`:

```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
		wantNode *node
	}{
		// 普通节点的参数路由子节点测试用例
		{
			name:    "order detail id",
			method:  http.MethodGet,
			path:    "/order/detail/123",
			isFound: true,
			wantNode: &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.wantNode.equal(foundNode)
			assert.True(t, found, msg)
		})
	}
}
```

单测顺利通过


# 3.10 路由树-参数路径之校验

本节课工程结构如下:

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

0 directories, 8 files
```

## PART1. 参数路由与通配符路由共存问题

### 1.1 修改逻辑

上节课中说过,我们的设计中是不准备支持同样的参数路径和通配符匹配一起注册的(即`/user/*`和`/user/:id`).

所以从逻辑上来讲,只需要实现:**注册参数路由时检测通配符路由是否存在;注册通配符路由时检测参数路由是否存在.若对方存在,则不允许注册**即可.

`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 {
			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
}
```

### 1.2 测试

`router_test.go`:

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

	// step2. 断言非法用例
	assert.Panicsf(t, func() {
		r.addRoute(http.MethodGet, "/order/detail/:id", mockHandleFunc)
	}, "web: 非法路由,已有通配符路由.不允许同时注册通配符路由和参数路由")
}

// TestRouter_findRoute_wildcard_and_param_coexist 测试针对注册通配符路由时,已有参数路由的情况
func TestRouter_findRoute_wildcard_and_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/*", mockHandleFunc)
	}, "web: 非法路由,已有参数路由.不允许同时注册通配符路由和参数路由")
}
```

以上两个测试用例均可顺利通过.


# 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")
}
```


# 3.12 路由树-总结与面试要点

本节课工程结构如下:

```
(base) yanglei@yuanhong 17-summary % 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. 注册路由的注意事项

* 已经注册了的路由,无法被覆盖.例如`/user/home`注册两次,会冲突
* `path`必须以`/`开始并且结尾不能有`/`,中间也不允许有连续的`/`
* 不能在同一个位置注册不同的参数路由.例如`/user/:id`和`/user/:name`冲突
* 不能在同一个位置同时注册通配符路由和参数路由.例如`/user/:id`和`/user/*`冲突
* 同名路径参数,在路由匹配的时候,值会被覆盖.例如`/user/:id/abc/:id`,那么`/user/123/abc/456`,最终`id = 456`
  * TODO:这里是可以实现把2个id都记录下来的,只需要将`matchNode.pathParams`的类型修改为`map[string][]string`即可.下去自己试一下
  * TODO:这种情况也可以考虑panic掉

把以上内容作为注释放在`addRoute()`方法上,那么但凡用户发现panic的时候看一眼这个注释,都知道出了什么问题

`router.go`:

```go
// AddRoute 注册路由到路由森林中的路由树上
// 其中path为路由的路径.该路径:
// 1. 不得为空字符串
// 2. 必须以"/"开头
// 3. 不能以"/"结尾
// 4. 不能包含连续的"/"
// - 已经注册了的路由,无法被覆盖.例如`/user/home`注册两次,会冲突
// - `path`必须以`/`开始并且结尾不能有`/`,中间也不允许有连续的`/`
// - 不能在同一个位置注册不同的参数路由.例如`/user/:id`和`/user/:name`冲突
// - 不能在同一个位置同时注册通配符路由和参数路由.例如`/user/:id`和`/user/*`冲突
// - 同名路径参数,在路由匹配的时候,值会被覆盖.例如`/user/:id/abc/:id`,那么`/user/123/abc/456`,最终`id = 456`
func (r *router) addRoute(method string, path string, handleFunc HandleFunc)
```

## PART2. 为什么在注册路由用`panic`?

可以看到在`addRoute()`方法中有大量的panic出现.

那么问题来了:

* 可不可以返回`error`?
* 应不应该返回`error`?

### 2.1 可不可以返回`error`?

可以返回,但没必要.因为框架的使用者根本不会去处理的.注册路由都失败了,谁还有空去处理注册路由的报错信息呢?

### 2.2 应不应该返回`error`?

其实不应该.从框架使用者的角度上来看,用户**必须完成注册路由的步骤,才能启动HTTPServer**.那么在注册路由时,采用`panic`就是没问题的,因为这时应用还没运行.

注册路由时`panic`总好过应用启动了再`panic`.

## PART3. 路由树是线程安全的吗?

显然不是线程安全的.我们要求用户必须要注册完路由才能启动`HTTPServer`.而正常的用法都是在启动之前**依次**注册路由,不存在并发场景.

换言之,这是一个顺序写并发读的场景.并不会出问题.并发读写才会引入线程安全问题.

至于运行期间动态注册路由,没必要支持.这是典型的**为了解决1%的问题,引入99%的代码**.

之所以不将路由树做成线程安全的,也是为了性能,因为一旦涉及到加锁/原子操作的情况,性能就下来了.

## PART4. 面试要点(一)

### 4.1 路由树算法的实现?

**核心就是前缀树**.前缀的意思就是,两个节点共同的前缀,将会被抽取出来作为父节点.在我们的实现中是按照`/`来切割,每一段作为一个节点.

### 4.2 路由匹配的优先级?

本质上这是和Web框架相关的.在我们的设计里面是`静态匹配 > 路径参数 > 通配符匹配`.但实际上这个规则意义不大,因为路径参数和通配符匹配是互斥的

### 4.3 路由查找会回溯吗?

这也是和Web框架相关的,我们在课程上是不支持的.在这里可以简单描述可回溯和不可回溯之间的区别,可以是用课程中的例子`/user/123/home`和`/user/*/*`.课程中不支持是因为这个特性非常鸡肋

### 4.4 Web框架是怎么组织路由树的?

一个HTTP动词就是一颗路由树.也可以考虑所有HTTP动词共用一颗路由树,每个节点标记自己支持的HTTP方法.前者是比较主流的

TODO:第二种可以尝试去写

## PART5. 面试要点(二)

### 5.1 路由查找的性能受什么影响? 或者说怎么评估路由查找的性能?

**核心是看路由树的高度,次要因素是路由树的宽度(想想我们的`node.children`字段)**.

但是我们实现的时候`node.children`字段用的是`map`来实现的,所以宽度对我们的实现影响不大.但是如果使用`slice`实现,宽度影响就比较大了.

GIN的最长前缀树,则树会更高;我们的实现中,树会更宽.

如果你不是为了装逼,不要写GIN的那种路由树,因为代码很复杂,且很难读懂.

### 5.2 路由树是线程安全的吗?

严格来说也是跟Web框架相关的.大多数都不是线程安全的,这是为了性能.所以才要求大家一定要先注册路由,后启动Web服务器.如果你有运行期间动态添加路由的需求,只需要利用装饰器模式,就可以将一个线程不安全的封装为线程安全的路由树.

### 5.3 具体匹配方式的实现原理?

课程上我们实现了静态匹配、通配符匹配和路径匹配，作业里面要求大家照着实现一个正则匹配.**其实核心就是划定优先级,然后一种种匹配方式挨个匹配过去**.


# PART04.课后复习


# 4.01 课后复习-Server

本节课工程结构:

```
(base) yanglei@yuanhong v4-rc % tree ./
./

0 directories, 0 files
```

## PART1. Server

### 1.1 接口定义

`serverInterface.go`:

```go
package v4_rc

import "net/http"

// ServerInterface 服务器实体接口
// 用于定义服务器实体的行为
type ServerInterface interface {
	// Handler 组合http.Handler接口
	http.Handler
	// Start 启动服务器
	Start(addr string) error
}
```

这里组合`http.Handler`,是为了最终调用`http.Serve()`,使得该方法的第2个参数是我们自己实现的`http.Handler`接口的实现.这一点我当时整理笔记时其实是不理解的,我复习时也发现我解释不清楚这件事.故补充.

### 1.2 实现接口的`Start()`方法

#### 1.2.1 实现`Start()`方法

`server.go`:

```go
package v4_rc

import (
	"net"
	"net/http"
)

type Server struct {
}

// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	panic("implement me")
}

// Start 启动服务器
func (s *Server) Start(addr string) error {
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	return http.Serve(listener, s)
}
```

在`net.Listen()`和`http.Serve()`之间做框架的操作,例如生命周期回调.

#### 1.2.2 测试

`server_test.go`:

```go
package v4_rc

import (
	"testing"
)

// TestServer_Start 测试服务器启动
func TestServer_Start(t *testing.T) {
	s := &Server{}
	err := s.Start(":8081")
	if err != nil {
		t.Fatal(err)
	}
}
```

### 1.3 定义注册路由的方法`addRoute()`

#### 1.3.1 定义`Context`

`context.go`:

```go
package v4_rc

type Context struct {
}
```

这里其实是为了定义`HandleFunc`,而定义`HandleFunc`是为了定义`AddRoute()`.

#### 1.3.2 定义`HandleFunc`

`handle_func.go`

```go
package v4_rc

type HandleFunc func(ctx *Context)
```

#### 1.3.3 在接口上定义`addRoute()`

`server_interface.go`:

```go
package v4_rc

import "net/http"

// ServerInterface 服务器实体接口
// 用于定义服务器实体的行为
type ServerInterface interface {
	// Handler 组合http.Handler接口
	http.Handler
	// Start 启动服务器
	Start(addr string) error

	// addRoute 注册路由
	addRoute(method string, pattern string, handler HandleFunc)
}
```

#### 1.3.4 在实现上定义`addRoute()`

`server.go`:

```go
package v4_rc

import (
	"net"
	"net/http"
)

type Server struct {
}

// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	panic("implement me")
}

// Start 启动服务器
func (s *Server) Start(addr string) error {
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	return http.Serve(listener, s)
}

// addRoute 注册路由
func (s *Server) addRoute(method string, pattern string, handler HandleFunc) {
	panic("implement me")
}
```

#### 1.3.5 定义`addRoute()`的衍生方法

或者也可以说是暴露给使用者的方法

`server.go`:

```go
package v4_rc

import (
	"net"
	"net/http"
)

type Server struct {
}

// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	panic("implement me")
}

// Start 启动服务器
func (s *Server) Start(addr string) error {
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	return http.Serve(listener, s)
}

// addRoute 注册路由
func (s *Server) addRoute(method string, pattern string, handler HandleFunc) {
	panic("implement me")
}

// GET 注册GET路由
func (s *Server) GET(path string, handler HandleFunc) {
	s.addRoute(http.MethodGet, path, handler)
}

// POST 注册POST路由
func (s *Server) POST(path string, handler HandleFunc) {
	s.addRoute(http.MethodPost, path, handler)
}
```

## PART2. 完成Server时的工程结构

```
(base) yanglei@yuanhong v4-rc % tree ./
./
├── context.go
├── handle_func.go
├── server.go
├── server_interface.go
└── server_test.go

0 directories, 5 files
```

## 附录

### 1. 可以魔改的点

* 将addr作为Server的一个成员属性
* 我看原生的`http.Server`结构体为了支持HTTPS,组合了一个`*tls.Config`,可以尝试


# 4.02 课后复习-Route

本节课工程结构:

```
(base) yanglei@yuanhong v4-rc % tree ./
./
├── context.go
├── handle_func.go
├── server.go
├── server_interface.go
└── server_test.go

0 directories, 5 files
```

## PART1. 定义路由森林与节点

### 1.1 定义节点

新建文件`node.go`:

```go
package v4_rc

// node 路由树中的节点
type node struct {
	// pattern 路由路径
	pattern  string
	// children 子节点 key为子节点的路由路径 value为路径对应子节点
	children map[string]*node
	// HandleFunc 路由对应的处理函数
	HandleFunc
}
```

### 1.2 定义路由森林

新建文件`router.go`:

```go
package v4_rc

// router 路由森林
type router struct {
	// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
	trees map[string]*node
}
```

### 1.3 定义添加路由的操作

`router.go`:

```go
package v4_rc

// router 路由森林
type router struct {
	// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
	trees map[string]*node
}

// addRoute 添加路由
func (r *router) addRoute(method string, pattern string, handle HandleFunc) {
	panic("implement me")
}
```

## PART2. 组合路由森林与Server

`server_interface.go`:

```go
package v4_rc

import "net/http"

// ServerInterface 服务器实体接口
// 用于定义服务器实体的行为
type ServerInterface interface {
	// Handler 组合http.Handler接口
	http.Handler
	// Start 启动服务器
	Start(addr string) error
}
```

`server.go`:

```go
package v4_rc

import (
	"net"
	"net/http"
)

type Server struct {
	*router
}

// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	s.serve(ctx)

	panic("implement me")
}

// serve 查找路由树并执行匹配到的节点所对应的处理函数
func (s *Server) serve(ctx *Context) {
	panic("implement me")
}

// Start 启动服务器
func (s *Server) Start(addr string) error {
	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}

	return http.Serve(listener, s)
}

// GET 注册GET路由
func (s *Server) GET(path string, handler HandleFunc) {
	s.addRoute(http.MethodGet, path, handler)
}

// POST 注册POST路由
func (s *Server) POST(path string, handler HandleFunc) {
	s.addRoute(http.MethodPost, path, handler)
}
```

## PART3. 静态路由注册

### 3.1 编写测试方法

#### 3.1.1 构造路由树

`router_test.go`:

```go
package v4_rc

import (
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

#### 3.1.2 断言路由森林

* 若2个路由森林中的路由树数量不同,则不相等
* 如果目标router中没有对应HTTP动词的路由树(例:目标路由森林中有GET/POST/PUT这3棵路由树,而期望路由森林中有GET/POST/DELETE这3棵路由树),则不相等

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// TODO: implement me
	return "", false
}
```

#### 3.1.3 断言路由树

* 若2个节点中有一个为nil,则不相等
* 若2个节点的path不同,则不相等
* 若2个节点的子节点数量不同,则不相等
* 若2个节点的HandleFunc类型不同,则不相等
* 比对2个节点的子节点映射:
  * 若源节点的子节点映射中,存在目标节点中没有的子节点,则不相同
    * 注:因为上边已经比对过2个节点的子节点数量了,所以只要能通过这个步骤的比对,那么2个节点的子节点必然是一一对应的
  * 两个path相同的节点递归比对

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点的处理函数为: %v, 目标节点的处理函数为: %v", wantHandler, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

### 3.2 添加测试用例

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点的处理函数为: %v, 目标节点的处理函数为: %v", wantHandler, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

### 3.3 实现addRoute方法

#### 3.3.1 根据method查找路由树,不存在则创建

`router.go`:

```go
package v4_rc

import (
	"strings"
)

// router 路由森林
type router struct {
	// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
	trees map[string]*node
}

func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
	// step1. 查找路由树,不存在则创建
	root, exist := r.trees[method]
	if !exist {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}
}
```

#### 3.3.2 对根节点做特殊处理

此处要特殊处理根节点的原因在于:后续按`/`分割path后,根节点就直接被分割没了(变成"")了.所以要特殊处理根节点

`router.go`:

```go
package v4_rc

import (
	"strings"
)

// router 路由森林
type router struct {
	// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
	trees map[string]*node
}

func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
	// step1. 查找路由树,不存在则创建
	root, exist := r.trees[method]
	if !exist {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 在根节点上查找子节点 不存在则创建
	// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
	if path == "/" {
		root.HandleFunc = handle
		return
	}
}
```

#### 3.3.3 从根节点开始逐层查找目标节点,找到目标节点后添加HandleFunc

`node.go`:

```go
package v4_rc

// node 路由树中的节点
type node struct {
	// path 路由路径
	path string
	// children 子节点 key为子节点的路由路径 value为路径对应子节点
	children map[string]*node
	// HandleFunc 路由对应的处理函数
	HandleFunc
}

// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
	if n.children == nil {
		n.children = make(map[string]*node)
	}

	target, exist := n.children[segment]
	if !exist {
		// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
		target = &node{
			path: segment,
		}
		n.children[segment] = target
		return target
	}

	// 当前节点的子节点映射中存在目标子节点 则直接返回
	return target
}
```

`router.go`:

```go
package v4_rc

import (
	"strings"
)

// router 路由森林
type router struct {
	// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
	trees map[string]*node
}

func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
	// step1. 查找路由树,不存在则创建
	root, exist := r.trees[method]
	if !exist {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 在根节点上查找子节点 不存在则创建
	// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
	if path == "/" {
		root.HandleFunc = handle
		return
	}

	// step2.2 从根节点开始 逐层查找
	target := root
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	for _, pathSegment := range pathSegments {
		// 在当前节点上查找子节点
		child := target.findOrCreate(pathSegment)
		target = child
	}

	// 为目标节点创建HandleFunc
	target.HandleFunc = handle
}
```

此时运行测试用例即可顺利通过测试

### 3.4 测试用例

#### 3.4.1 对根节点的测试用例

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg := fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

#### 3.4.2 前导/user节点的测试用例

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
				},
				HandleFunc: mockHandleFunc,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

#### 3.4.3 /order/detail节点的测试用例

为测试路由树中间有不存在的节点时,是否符合预期

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

#### 3.4.4 其他HTTP动词的测试用例

**a. /login**

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodPost,
			path:   "/login",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
			http.MethodPost: &node{
				path: "/",
				children: map[string]*node{
					"login": &node{
						path:       "login",
						children:   nil,
						HandleFunc: mockHandleFunc,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

**b. /order/create**

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodPost,
			path:   "/login",
		},
		{
			method: http.MethodPost,
			path:   "/order/create",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
			http.MethodPost: &node{
				path: "/",
				children: map[string]*node{
					"login": &node{
						path:       "login",
						children:   nil,
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"create": &node{
								path:       "create",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

#### 3.4.5 非法用例

**a. path为空字符串**

`router.go`:

```go
package v4_rc

import (
	"strings"
)

// router 路由森林
type router struct {
	// trees 路由森林 key为HTTP动词 value为HTTP对应路由树的根节点
	trees map[string]*node
}

func newRouter() *router {
	return &router{
		trees: map[string]*node{},
	}
}

// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
	msg, ok := r.checkPath(path)
	if !ok {
		panic(msg)
	}

	// step1. 查找路由树,不存在则创建
	root, exist := r.trees[method]
	if !exist {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 在根节点上查找子节点 不存在则创建
	// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
	if path == "/" {
		root.HandleFunc = handle
		return
	}

	// step2.2 从根节点开始 逐层查找
	target := root
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	for _, pathSegment := range pathSegments {
		// 在当前节点上查找子节点
		child := target.findOrCreate(pathSegment)
		target = child
	}

	// 为目标节点创建HandleFunc
	target.HandleFunc = handle
}

// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
	if path == "" {
		return "web: 路由不能为空字符串", false
	}

	return "", true
}
```

`router_test.go`:

```go
package v4_rc

import (
	"fmt"
	"github.com/stretchr/testify/assert"
	"net/http"
	"reflect"
	"testing"
)

type TestNode struct {
	method string
	path   string
}

func TestRouter_AddRoute(t *testing.T) {
	// step1. 构造路由森林
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/",
		},
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/user/home",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodPost,
			path:   "/login",
		},
		{
			method: http.MethodPost,
			path:   "/order/create",
		},
	}
	targetRouter := newRouter()
	mockHandleFunc := func(ctx *Context) {}

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

	// step2. 验证路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path: "user",
						children: map[string]*node{
							"home": &node{
								path:       "home",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandleFunc,
			},
			http.MethodPost: &node{
				path: "/",
				children: map[string]*node{
					"login": &node{
						path:       "login",
						children:   nil,
						HandleFunc: mockHandleFunc,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"create": &node{
								path:       "create",
								children:   nil,
								HandleFunc: mockHandleFunc,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(targetRouter)
	assert.True(t, ok, msg)
}

func (r *router) equal(target *router) (msg string, ok bool) {
	// step1. 比较路由森林中的路由树数量
	wantLen := len(r.trees)
	targetLen := len(target.trees)

	if wantLen != targetLen {
		msg = fmt.Sprintf("路由森林中的路由树数量不等, 期望路由树的数量为: %d, 目标路由树的数量为: %d", wantLen, targetLen)
		return msg, false
	}

	// step2. 比对2个路由森林中的路由树HTTP动词是否相同
	for method, tree := range r.trees {
		dstTree, ok := target.trees[method]
		if !ok {
			msg = fmt.Sprintf("目标路由森林中不存在HTTP动词为: %s 的路由树", method)
			return msg, false
		}

		// step3. 比对2个路由树中的结构是否相同
		msg, ok = tree.equal(dstTree)
		if !ok {
			return msg, false
		}
	}

	return "", true
}

func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step5. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step5.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step5.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}

func TestRouter_Illegal_Path(t *testing.T) {
	r := newRouter()
	mockHandle := func(ctx *Context) {}

	nilPathFunc := func() { r.addRoute(http.MethodGet, "", mockHandle) }
	assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")
}
```

TODO:assert.Panicsf

**b. path不是以`/`开头**

`router.go`:

```go
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
	if path == "" {
		return "web: 路由不能为空字符串", false
	}

	if path[0] != '/' {
		return "web: 路由必须以/开头", false
	}

	return "", true
}
```

`router_test.go`:

```go
func TestRouter_Illegal_Path(t *testing.T) {
	r := newRouter()
	mockHandle := func(ctx *Context) {}

	// 路由为空的测试用例
	nilPathFunc := func() {
		r.addRoute(http.MethodGet, "", mockHandle)
	}
	assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")

	// 路由不是以`/`开头的测试用例
	incorrectFirstCharacter := func() {
		r.addRoute(http.MethodGet, "login", mockHandle)
	}
	assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")
}
```

**c. path以`/`结尾**

`router.go`:

```go
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
	if path == "" {
		return "web: 路由不能为空字符串", false
	}

	if path[0] != '/' {
		return "web: 路由必须以/开头", false
	}

	if path != "/" && path[len(path)-1] == '/' {
		return "web: 路由不能以/结尾", false
	}

	return "", true
}
```

`router_test.go`:

```go
func TestRouter_Illegal_Path(t *testing.T) {
	r := newRouter()
	mockHandle := func(ctx *Context) {}

	// 路由为空的测试用例
	nilPathFunc := func() {
		r.addRoute(http.MethodGet, "", mockHandle)
	}
	assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")

	// 路由不是以`/`开头的测试用例
	incorrectFirstCharacter := func() {
		r.addRoute(http.MethodGet, "login", mockHandle)
	}
	assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")

	// 路由以`/`结尾的测试用例
	incorrectLastCharacter := func() {
		r.addRoute(http.MethodGet, "/login/", mockHandle)
	}
	assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")
}
```

**d. path中包含连续的`/`**

`router.go`:

```go
// checkPath 检测路由是否合法
// 此处没有返回error 是因为设计上如果路由不合法 直接panic而非报错
// 所以此方法只返回 表示是否合法的标量以及表示不合法原因的字符串即可
func (r *router) checkPath(path string) (msg string, ok bool) {
	if path == "" {
		return "web: 路由不能为空字符串", false
	}

	if path[0] != '/' {
		return "web: 路由必须以/开头", false
	}

	if path != "/" {
		if path[len(path)-1] == '/' {
			return "web: 路由不能以/结尾", false
		}

		path = strings.TrimLeft(path, "/")
		pathSegments := strings.Split(path, "/")
		for _, pathSegment := range pathSegments {
			if pathSegment == "" {
				return "web: 路由中不能出现连续的/", false
			}
		}
	}

	return "", true
}
```

`router_test.go`:

```go
func TestRouter_Illegal_Path(t *testing.T) {
	r := newRouter()
	mockHandle := func(ctx *Context) {}

	// 路由为空的测试用例
	nilPathFunc := func() {
		r.addRoute(http.MethodGet, "", mockHandle)
	}
	assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")

	// 路由不是以`/`开头的测试用例
	incorrectFirstCharacter := func() {
		r.addRoute(http.MethodGet, "login", mockHandle)
	}
	assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")

	// 路由以`/`结尾的测试用例
	incorrectLastCharacter := func() {
		r.addRoute(http.MethodGet, "/login/", mockHandle)
	}
	assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")

	// 路由中出现了连续的/
	continuousSeparator := func() {
		r.addRoute(http.MethodGet, "/a//b", mockHandle)
	}
	assert.Panicsf(t, continuousSeparator, "web: 路由不能出现多个连续的/")
}
```

**e. 路由重复注册**

**e1. 根节点路由重复注册**

`router.go`:

```go
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
	msg, ok := r.checkPath(path)
	if !ok {
		panic(msg)
	}

	// step1. 查找路由树,不存在则创建
	root, exist := r.trees[method]
	if !exist {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 在根节点上查找子节点 不存在则创建
	// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
	if path == "/" {
		if root.HandleFunc != nil {
			msg = fmt.Sprintf("web: 路由冲突,重复注册路由 [%s]", path)
			panic(msg)
		}
		root.HandleFunc = handle
		return
	}

	// step2.2 从根节点开始 逐层查找
	target := root
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	for _, pathSegment := range pathSegments {
		// 在当前节点上查找子节点
		child := target.findOrCreate(pathSegment)
		target = child
	}

	// 为目标节点创建HandleFunc
	target.HandleFunc = handle
}
```

`router_test.go`:

```go
func TestRouter_Illegal_Path(t *testing.T) {
	r := newRouter()
	mockHandle := func(ctx *Context) {}

	// 路由为空的测试用例
	nilPathFunc := func() {
		r.addRoute(http.MethodGet, "", mockHandle)
	}
	assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")

	// 路由不是以`/`开头的测试用例
	incorrectFirstCharacter := func() {
		r.addRoute(http.MethodGet, "login", mockHandle)
	}
	assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")

	// 路由以`/`结尾的测试用例
	incorrectLastCharacter := func() {
		r.addRoute(http.MethodGet, "/login/", mockHandle)
	}
	assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")

	// 路由中出现了连续`/`的测试用例
	continuousSeparator := func() {
		r.addRoute(http.MethodGet, "/a//b", mockHandle)
	}
	assert.Panicsf(t, continuousSeparator, "web: 路由不能出现多个连续的/")

	// 路由重复注册的测试用例
	r.addRoute(http.MethodGet, "/", mockHandle)
	repeatRegisterRoute := func() {
		r.addRoute(http.MethodGet, "/", mockHandle)
	}
	assert.Panicsf(t, repeatRegisterRoute, "web: 路由冲突,重复注册路由 [/]")
}
```

**e2. 普通节点路由重复注册**

`router.go`:

```go
// addRoute 添加路由
func (r *router) addRoute(method string, path string, handle HandleFunc) {
	msg, ok := r.checkPath(path)
	if !ok {
		panic(msg)
	}

	// step1. 查找路由树,不存在则创建
	root, exist := r.trees[method]
	if !exist {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	// step2. 在根节点上查找子节点 不存在则创建
	// step2.1 由于按/切割后 第一个元素为"" 也就是说如果传入的path为"/" 需要特殊处理
	if path == "/" {
		if root.HandleFunc != nil {
			msg = fmt.Sprintf("web: 路由冲突,重复注册路由 [%s]", path)
			panic(msg)
		}
		root.HandleFunc = handle
		return
	}

	// step2.2 从根节点开始 逐层查找
	target := root
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	for _, pathSegment := range pathSegments {
		// 在当前节点上查找子节点
		child := target.findOrCreate(pathSegment)
		target = child
	}

	// 为目标节点创建HandleFunc
	if target.HandleFunc != nil {
		msg = fmt.Sprintf("web: 路由冲突,重复注册路由 [%s]", path)
		panic(msg)
	}
	target.HandleFunc = handle
}
```

`router_test.go`:

```go
func TestRouter_Illegal_Path(t *testing.T) {
	r := newRouter()
	mockHandle := func(ctx *Context) {}

	// 路由为空的测试用例
	nilPathFunc := func() {
		r.addRoute(http.MethodGet, "", mockHandle)
	}
	assert.Panicsf(t, nilPathFunc, "web: 路由不能为空字符串")

	// 路由不是以`/`开头的测试用例
	incorrectFirstCharacter := func() {
		r.addRoute(http.MethodGet, "login", mockHandle)
	}
	assert.Panicsf(t, incorrectFirstCharacter, "web: 路由必须以/开头")

	// 路由以`/`结尾的测试用例
	incorrectLastCharacter := func() {
		r.addRoute(http.MethodGet, "/login/", mockHandle)
	}
	assert.Panicsf(t, incorrectLastCharacter, "web: 路由不能以/结尾")

	// 路由中出现了连续`/`的测试用例
	continuousSeparator := func() {
		r.addRoute(http.MethodGet, "/a//b", mockHandle)
	}
	assert.Panicsf(t, continuousSeparator, "web: 路由不能出现多个连续的/")

	// 路由重复注册的测试用例
	// 根节点路由重复注册
	r.addRoute(http.MethodGet, "/", mockHandle)
	repeatRegisterRoute := func() {
		r.addRoute(http.MethodGet, "/", mockHandle)
	}
	assert.Panicsf(t, repeatRegisterRoute, "web: 路由冲突,重复注册路由 [/]")

	// 普通节点路由重复注册
	r.addRoute(http.MethodGet, "/user/login", mockHandle)
	repeatRegisterRoute = func() {
		r.addRoute(http.MethodGet, "/user/login", mockHandle)
	}
	assert.Panicsf(t, repeatRegisterRoute, "web: 路由冲突,重复注册路由 [/user/login]")
}
```

## PART4. 静态路由查找

### 4.1 编写测试函数

`router_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

### 4.2 定义测试用例的类型

我们需要这个类型能够告知我们如下信息:

* 在给定HTTP动词和path的前提下,是否找到了节点?
* 在给定HTTP动词和path的前提下,找到的节点和预期的节点是否相同?

```go
type TestCaseNode struct {
	name     string // name 子测试用例的名称
	method   string // method HTTP动词
	path     string // path 路由路径
	isFound  bool   // isFound 是否找到了节点
	wantNode *node  // wantNode 期望的路由节点
}
```

### 4.3 定义测试的过程

* step1. 判断在路由树中是否找到了节点
* step2. 判断找到的节点和预期的节点是否相同

`route_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user",
		},
	}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

	// step2. 构造测试用例
	testCases := []struct {
		// name 子测试用例的名称
		name string
		// method HTTP动词
		method string
		// path 路由路径
		path string
		// isFound 是否找到了节点
		isFound bool
		// wantNode 期望的路由节点
		wantNode *node
	}{}

	// step3. 测试是否找到节点
	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)
			// 3.1 判断在路由树中是否找到了节点
			if !found {
				return
			}

			// 3.2 判断找到的节点和预期的节点是否相同
			msg, equal := testCase.wantNode.equal(foundNode)
			assert.True(t, equal, msg)
		})
	}
}
```

### 4.4 构造测试用例

* HTTP动词对应的路由树不存在
* 完全命中
* 命中了path对应的节点,但HandleFunc为nil
* 根节点(特殊处理)
* path对应的节点不存在

### 4.5 根据测试用例开发findRoute()

#### 4.5.1 HTTP动词对应的路由树不存在

**a. 实现**

`route.go`:

```go
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
	_, ok := r.trees[method]
	if !ok {
		return nil, false
	}
	panic("implement me")
}
```

**b. 测试**

`route_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user",
		},
	}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

	// step2. 构造测试用例
	testCases := []struct {
		// name 子测试用例的名称
		name string
		// method HTTP动词
		method string
		// path 路由路径
		path string
		// isFound 是否找到了节点
		isFound bool
		// wantNode 期望的路由节点
		wantNode *node
	}{
		{
			name:     "Method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},
	}

	// step3. 测试是否找到节点
	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)
			// 3.1 判断在路由树中是否找到了节点
			if !found {
				return
			}

			// 3.2 判断找到的节点和预期的节点是否相同
			msg, equal := testCase.wantNode.equal(foundNode)
			assert.True(t, equal, msg)
		})
	}
}
```

#### 4.5.2 完全命中

**a. 实现**

* step1. 按`/`切割path
* step2. 从路由树的根节点开始,按"层次"查找节点
* step3. 没找到则返回`nil, false`即可

这里我一开始的实现是这样的:

`route.go`:

```go
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
	// HTTP动词对应的路由树不存在 直接返回nil false即可
	root, ok := r.trees[method]
	if !ok {
		return nil, false
	}

	//
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	target := root
	for _, pathSegment := range pathSegments {
		target, ok = target.children[pathSegment]
		if !ok {
			return nil, false
		}
	}
	return target, true
}
```

有2个问题:

1. 没有对`target.children`判空
2. "在当前节点下查找子节点"是一个独立完整的功能,应该是`node`结构体的方法

修改后的实现:

`node.go`:

```go
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找path为给定path值的节点
// 找到则返回节点 否则返回 nil, false
func (n *node) childOf(path string) (*node, bool) {
	if n.children == nil {
		return nil, false
	}

	child, found := n.children[path]
	if !found {
		return nil, false
	}

	return child, true
}
```

`route.go`:

```go
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
	// HTTP动词对应的路由树不存在 直接返回nil false即可
	root, ok := r.trees[method]
	if !ok {
		return nil, false
	}

	// 在路由树中逐层查找节点
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	target := root
	for _, pathSegment := range pathSegments {
		target, ok = target.children[pathSegment]
		if !ok {
			return nil, false
		}
	}
	return target, true
}
```

**b. 测试**

`route_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
	}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

	// step2. 构造测试用例
	testCases := []struct {
		// name 子测试用例的名称
		name string
		// method HTTP动词
		method string
		// path 路由路径
		path string
		// isFound 是否找到了节点
		isFound bool
		// wantNode 期望的路由节点
		wantNode *node
	}{
		{
			name:     "Method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},
		{
			name:    "completely match",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			wantNode: &node{
				path:       "detail",
				children:   nil,
				HandleFunc: mockHandle,
			},
		},
	}

	// step3. 测试是否找到节点
	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)
			// 3.1 判断在路由树中是否找到了节点
			if !found {
				return
			}

			// 3.2 判断找到的节点和预期的节点是否相同
			msg, equal := testCase.wantNode.equal(foundNode)
			assert.True(t, equal, msg)
		})
	}
}
```

#### 4.5.3 命中了path对应的节点,但HandleFunc为nil

这个case不需要做特殊的边缘条件检测.因为在`addRoute()`时我们也没有检测HandleFunc是否为空.换言之就是:既然在注册路由时允许使用者传入空的HandleFunc,那么在查找时命中了一个HandleFunc为nil的节点就不算是错误

**a. 测试**

`route_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
	}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

	// step2. 构造测试用例
	testCases := []struct {
		// name 子测试用例的名称
		name string
		// method HTTP动词
		method string
		// path 路由路径
		path string
		// isFound 是否找到了节点
		isFound bool
		// wantNode 期望的路由节点
		wantNode *node
	}{
		{
			name:     "Method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},
		{
			name:    "completely match",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			wantNode: &node{
				path:       "detail",
				children:   nil,
				HandleFunc: mockHandle,
			},
		},
		{
			name:    "nil handle func",
			method:  http.MethodGet,
			path:    "/order",
			isFound: true,
			wantNode: &node{
				path: "order",
				children: map[string]*node{
					"detail": &node{
						path:       "detail",
						children:   nil,
						HandleFunc: mockHandle,
					},
				},
				HandleFunc: nil,
			},
		},
	}

	// step3. 测试是否找到节点
	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)
			// 3.1 判断在路由树中是否找到了节点
			if !found {
				return
			}

			// 3.2 判断找到的节点和预期的节点是否相同
			msg, equal := testCase.wantNode.equal(foundNode)
			assert.True(t, equal, msg)
		})
	}
}
```

#### 4.5.4 根节点

**a. 实现**

`route.go`:

```go
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*node, bool) {
	// HTTP动词对应的路由树不存在 直接返回nil false即可
	root, ok := r.trees[method]
	if !ok {
		return nil, false
	}

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

	// 在路由树中逐层查找节点
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	for _, pathSegment := range pathSegments {
		target, ok = target.children[pathSegment]
		if !ok {
			return nil, false
		}
	}
	return target, true
}
```

**b. 测试**

`route_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodGet,
			path:   "/",
		},
	}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

	// step2. 构造测试用例
	testCases := []struct {
		// name 子测试用例的名称
		name string
		// method HTTP动词
		method string
		// path 路由路径
		path string
		// isFound 是否找到了节点
		isFound bool
		// wantNode 期望的路由节点
		wantNode *node
	}{
		{
			name:     "Method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},
		{
			name:    "completely match",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			wantNode: &node{
				path:       "detail",
				children:   nil,
				HandleFunc: mockHandle,
			},
		},
		{
			name:    "nil handle func",
			method:  http.MethodGet,
			path:    "/order",
			isFound: true,
			wantNode: &node{
				path: "order",
				children: map[string]*node{
					"detail": &node{
						path:       "detail",
						children:   nil,
						HandleFunc: mockHandle,
					},
				},
				HandleFunc: nil,
			},
		},
		{
			name:    "root node",
			method:  http.MethodGet,
			path:    "/",
			isFound: true,
			wantNode: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path:       "user",
						children:   nil,
						HandleFunc: mockHandle,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandle,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandle,
			},
		},
	}

	// step3. 测试是否找到节点
	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)
			// 3.1 判断在路由树中是否找到了节点
			if !found {
				return
			}

			// 3.2 判断找到的节点和预期的节点是否相同
			msg, equal := testCase.wantNode.equal(foundNode)
			assert.True(t, equal, msg)
		})
	}
}
```

#### 4.5.5 path对应的节点不存在

**a. 测试**

`route_test.go`:

```go
func TestRouter_FindRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/user",
		},
		{
			method: http.MethodGet,
			path:   "/order/detail",
		},
		{
			method: http.MethodGet,
			path:   "/",
		},
	}
	r := newRouter()
	mockHandle := func(ctx *Context) {}

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

	// step2. 构造测试用例
	testCases := []struct {
		// name 子测试用例的名称
		name string
		// method HTTP动词
		method string
		// path 路由路径
		path string
		// isFound 是否找到了节点
		isFound bool
		// wantNode 期望的路由节点
		wantNode *node
	}{
		{
			name:     "Method not found",
			method:   http.MethodDelete,
			path:     "/user",
			isFound:  false,
			wantNode: nil,
		},
		{
			name:    "completely match",
			method:  http.MethodGet,
			path:    "/order/detail",
			isFound: true,
			wantNode: &node{
				path:       "detail",
				children:   nil,
				HandleFunc: mockHandle,
			},
		},
		{
			name:    "nil handle func",
			method:  http.MethodGet,
			path:    "/order",
			isFound: true,
			wantNode: &node{
				path: "order",
				children: map[string]*node{
					"detail": &node{
						path:       "detail",
						children:   nil,
						HandleFunc: mockHandle,
					},
				},
				HandleFunc: nil,
			},
		},
		{
			name:    "root node",
			method:  http.MethodGet,
			path:    "/",
			isFound: true,
			wantNode: &node{
				path: "/",
				children: map[string]*node{
					"user": &node{
						path:       "user",
						children:   nil,
						HandleFunc: mockHandle,
					},
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:       "detail",
								children:   nil,
								HandleFunc: mockHandle,
							},
						},
						HandleFunc: nil,
					},
				},
				HandleFunc: mockHandle,
			},
		},
		{
			name:     "path not found",
			method:   http.MethodGet,
			path:     "/login",
			isFound:  false,
			wantNode: nil,
		},
	}

	// step3. 测试是否找到节点
	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)
			// 3.1 判断在路由树中是否找到了节点
			if !found {
				return
			}

			// 3.2 判断找到的节点和预期的节点是否相同
			msg, equal := testCase.wantNode.equal(foundNode)
			assert.True(t, equal, msg)
		})
	}
}
```

## PART5. route集成至Server

### 5.1 实现

`server.go`:

```go
// ServeHTTP 是http.Handler接口的方法 此处必须先写个实现 不然Server不是http.Handler接口的实现
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	s.serve(ctx)
}

// serve 查找路由树并执行匹配到的节点所对应的处理函数
func (s *HTTPServer) serve(ctx *Context) {
	method := ctx.Req.Method
	path := ctx.Req.URL.Path
	targetNode, found := s.router.findRoute(method, path)
	if !found || targetNode.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		_, _ = ctx.Resp.Write([]byte("not found"))
		return
	}
	targetNode.HandleFunc(ctx)
}
```

### 5.2 测试

`server_test.go`:

```go
package v4_rc

import (
	"net/http"
	"testing"
)

// TestServer_Start 测试服务器启动
func TestServer_Start(t *testing.T) {
	s := NewHTTPServer()
	handleFunc := func(ctx *Context) {
		ctx.Resp.Write([]byte("hello order detail"))
	}
	s.addRoute(http.MethodGet, "/order/detail", handleFunc)
	err := s.Start(":8081")
	if err != nil {
		t.Fatal(err)
	}
}
```

## PART6. 通配符路由的注册与查找

### 6.1 通配符路由的定义与设计

#### 6.1.1 通配符路由的定义

通配符路由:用`*`表达匹配任何路径

#### 6.1.2 通配符路由的设计

1. 一个`*`只能表达1段路由
2. 不做可回溯的路由匹配机制

### 6.2 通配符路由的注册

#### 6.2.1 修改`node`的结构

`node.go`:

```go
// node 路由树中的节点
type node struct {
	// path 路由路径
	path string
	// children 子节点 key为子节点的路由路径 value为路径对应子节点
	children map[string]*node
	// wildcardChild 通配符子节点
	wildcardChild *node
	// HandleFunc 路由对应的处理函数
	HandleFunc
}
```

#### 6.2.2 定义测试用例

`route_test.go`:

```go
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 断言路由树
	wantRouter := &router{trees: map[string]*node{
		"/": &node{
			path: "/",
			children: map[string]*node{
				"order": &node{
					path:     "order",
					children: nil,
					wildcardChild: &node{
						path:          "*",
						children:      nil,
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: nil,
				},
			},
			wildcardChild: nil,
			HandleFunc:    nil,
		},
	}}

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}
```

根据debug的结果可知:应该把`*`创建在`wildcardChild`字段上,现在则是创建在了`children`字段上

#### 6.2.3 修改创建子节点的逻辑

正确的逻辑是:当路径为`*`时,将节点创建在`wildcardChild`字段上

`node.go`:

```go
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
	// 若路径为* 则查找或创建通配符子节点
	if segment == "*" {
		if n.wildcardChild == nil {
			n.wildcardChild = &node{
				path: "*",
			}
		}
		return n.wildcardChild
	}

	if n.children == nil {
		n.children = make(map[string]*node)
	}

	target, exist := n.children[segment]
	if !exist {
		// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
		target = &node{
			path: segment,
		}
		n.children[segment] = target
		return target
	}

	// 当前节点的子节点映射中存在目标子节点 则直接返回
	return target
}
```

#### 6.2.4 修改判断子节点相等的逻辑

由于新增了通配符子节点字段,所以对于2个节点,也要比对各自的通配符子节点是否相同

`route_test.go`:

```go
func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比较2个节点的通配符子节点是否相同
	if n.wildcardChild != nil {
		if target.wildcardChild == nil {
			msg = fmt.Sprintf("目标节点的通配符子节点为空")
			return msg, false
		}
		_, equal := n.wildcardChild.equal(target.wildcardChild)
		if !equal {
			msg = fmt.Sprintf("期望节点 %s 的通配符子节点与目标节点 %s 的通配符子节点不等", n.path, target.path)
			return msg, false
		}
	}

	// step5. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step6. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step6.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step6.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

此时再跑测试用例,就可以跑通了

#### 6.2.5 添加其他测试用例

**6.2.5.1 根节点的通配符子节点**

`route_test.go`:

```go
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 断言路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"order": &node{
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path:          "*",
					children:      nil,
					wildcardChild: nil,
					HandleFunc:    mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}
```

**6.2.5.2 通配符子节点的通配符子节点**

`route_test.go`:

```go
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*",
		},
		// 通配符子节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*/*",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 断言路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"order": &node{
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path:     "*",
					children: nil,
					wildcardChild: &node{
						path:          "*",
						children:      nil,
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}
```

**6.2.5.3 通配符子节点的普通子节点**

`route_test.go`:

```go
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*",
		},
		// 通配符子节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*/*",
		},
		// 通配符子节点的普通子节点
		{
			method: http.MethodGet,
			path:   "/*/get",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 断言路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"order": &node{
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path: "*",
					children: map[string]*node{
						"get": &node{
							path:          "get",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
					},
					wildcardChild: &node{
						path:          "*",
						children:      nil,
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}
```

**6.2.5.4 通配符子节点的普通子节点的通配符子节点**

`route_test.go`:

```go
func TestRouter_wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		// 普通节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		// 根节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*",
		},
		// 通配符子节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*/*",
		},
		// 通配符子节点的普通子节点
		{
			method: http.MethodGet,
			path:   "/*/get",
		},
		// 通配符子节点的普通子节点的通配符子节点
		{
			method: http.MethodGet,
			path:   "/*/order/*",
		},
	}

	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandleFunc)
	}

	// step2. 断言路由树
	wantRouter := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"order": &node{
						path:     "order",
						children: nil,
						wildcardChild: &node{
							path:          "*",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						HandleFunc: nil,
					},
				},
				wildcardChild: &node{
					path: "*",
					children: map[string]*node{
						"get": &node{
							path:          "get",
							children:      nil,
							wildcardChild: nil,
							HandleFunc:    mockHandleFunc,
						},
						"order": &node{
							path:     "order",
							children: nil,
							wildcardChild: &node{
								path:          "*",
								children:      nil,
								wildcardChild: nil,
								HandleFunc:    mockHandleFunc,
							},
							HandleFunc: nil,
						},
					},
					wildcardChild: &node{
						path:          "*",
						children:      nil,
						wildcardChild: nil,
						HandleFunc:    mockHandleFunc,
					},
					HandleFunc: mockHandleFunc,
				},
				HandleFunc: nil,
			},
		},
	}

	msg, ok := wantRouter.equal(r)
	assert.True(t, ok, msg)
}
```

### 6.3 通配符路由的匹配

#### 6.3.1 实现

思路比较简单:

* 若当前节点的children映射为空,则有可能匹配到通配符子节点
* 若在当前节点的children映射中没有找到path对应的子节点,则有可能匹配到通配符子节点
  * 此处说"有可能匹配到",是因为还有一种可能是通配符子节点为空,这种情况就属于没匹配到了

`node.go`:

```go
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找path为给定path值的节点
// 找到则返回节点 否则返回 nil, false
func (n *node) childOf(path string) (*node, bool) {
	if n.children == nil {
		return n.wildcardChild, n.wildcardChild != nil
	}

	child, found := n.children[path]
	if !found {
		return n.wildcardChild, n.wildcardChild != nil
	}

	return child, true
}
```

#### 6.3.2 测试

**a. 匹配普通节点的通配符子节点**

`route_test.go`:

```go
func TestRouter_FindRoute_Wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
	}

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

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

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

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

			msg, equal := testCase.wantNode.equal(targetNode)
			assert.True(t, equal, msg)
		})
	}
}
```

**b. 匹配普通节点下普通子节点和通配符子节点共存**

```go
func TestRouter_FindRoute_Wildcard(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []TestNode{
		{
			method: http.MethodGet,
			path:   "/order/*",
		},
		{
			method: http.MethodGet,
			path:   "/order/create",
		},
	}

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

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

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

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

			msg, equal := testCase.wantNode.equal(targetNode)
			assert.True(t, equal, msg)
		})
	}
}
```

**c. server组合route时的通配符匹配**

`server_test.go`:

```go
package v4_rc

import (
	"net/http"
	"testing"
)

// TestServer_Start 测试服务器启动
func TestServer_Start(t *testing.T) {
	s := NewHTTPServer()

	wildcardHandleFunc := func(ctx *Context) {
		ctx.Resp.Write([]byte("hello order wildcard"))
	}
	s.addRoute(http.MethodGet, "/order/*", wildcardHandleFunc)

	handleFunc := func(ctx *Context) {
		ctx.Resp.Write([]byte("hello order detail"))
	}
	s.addRoute(http.MethodGet, "/order/detail", handleFunc)

	err := s.Start(":8081")
	if err != nil {
		t.Fatal(err)
	}
}
```

## PART7. 参数路由的注册与查找

### 7.1 参数路由的定义与设计

#### 7.1.1 参数路由的定义

参数路由:就是指在路由中带上参数,同时这些参数对应的值可以被业务取出来使用.在我们的设计中用`:参数名`的形式表示路由参数

例:`/user/:id`,如果输入路径`/user/123`,则会命中这个路由`/user/:id`,并且在业务函数中可以取到变量`id = 123`

#### 7.1.2 参数路径的设计

**是否允许同样的参数路由和通配符路由一起注册?**

例如同时注册`/user/:id`和`/user/*`一起注册?

可以允许,但没必要,且用户也不该设计这种路由

### 7.2 实现参数路由节点的创建

#### 7.2.1 修改node的结构

`node.go`

```go
// node 路由树中的节点
type node struct {
	path          string           // path 路由路径
	children      map[string]*node // children 子节点 key为子节点的路由路径 value为路径对应子节点
	wildcardChild *node            // wildcardChild 通配符子节点
	paramChild    *node            // paramChild 参数路由子节点
	HandleFunc                     // HandleFunc 路由对应的处理函数
}
```

#### 7.2.2 定义测试用例

`router_test.go`

```go
func TestRouter_addParamRoute(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. 验证路由树
	wantRoute := &router{
		trees: map[string]*node{
			http.MethodGet: &node{
				path: "/",
				children: map[string]*node{
					"order": &node{
						path: "order",
						children: map[string]*node{
							"detail": &node{
								path:          "detail",
								children:      nil,
								wildcardChild: nil,
								paramChild: &node{
									path:          ":id",
									children:      nil,
									wildcardChild: nil,
									paramChild:    nil,
									HandleFunc:    mockHandleFunc,
								},
								HandleFunc: nil,
							},
						},
						wildcardChild: nil,
						paramChild:    nil,
						HandleFunc:    nil,
					},
				},
				wildcardChild: nil,
				paramChild:    nil,
				HandleFunc:    nil,
			},
		},
	}

	msg, equal := wantRoute.equal(r)
	assert.True(t, equal, msg)
}
```

#### 7.2.3 修改创建子节点的逻辑

`node.go`:

```go
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
	// 若路径以:开头 则查找或创建参数子节点
	if strings.HasPrefix(segment, ":") {
		if n.paramChild == nil {
			n.paramChild = &node{
				path: segment,
			}
		}
		return n.paramChild
	}

	// 若路径为* 则查找或创建通配符子节点
	if segment == "*" {
		if n.wildcardChild == nil {
			n.wildcardChild = &node{
				path: "*",
			}
		}
		return n.wildcardChild
	}

	if n.children == nil {
		n.children = make(map[string]*node)
	}

	target, exist := n.children[segment]
	if !exist {
		// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
		target = &node{
			path: segment,
		}
		n.children[segment] = target
		return target
	}

	// 当前节点的子节点映射中存在目标子节点 则直接返回
	return target
}
```

#### 7.2.4 修改判断子节点相等的逻辑

`router_test.go`:

```go
func (n *node) equal(target *node) (msg string, ok bool) {
	// step1. 目标节点为空 则必然不等
	if target == nil {
		msg = "目标节点为nil"
		return msg, false
	}

	// step2. 比较节点的路径是否相同
	if n.path != target.path {
		msg = fmt.Sprintf("节点的路径不等, 期望节点的路径为: %s, 目标节点的路径为: %s", n.path, target.path)
		return msg, false
	}

	// step3. 比较2个节点的子节点数量是否相同
	if len(n.children) != len(target.children) {
		msg = fmt.Sprintf("节点的子节点数量不等, 期望节点的子节点数量为: %d, 目标节点的子节点数量为: %d", len(n.children), len(target.children))
		return msg, false
	}

	// step4. 比对2个节点的参数子节点是否相同
	if n.paramChild != nil {
		if target.paramChild == nil {
			msg = fmt.Sprintf("目标节点的参数节点为空")
			return msg, false
		}
		_, equal := n.paramChild.equal(target.paramChild)
		if !equal {
			msg = fmt.Sprintf("期望节点 %s 的参数子节点与目标节点 %s 的参数子节点不等", n.path, target.path)
			return msg, false
		}
	}

	// step5. 比较2个节点的通配符子节点是否相同
	if n.wildcardChild != nil {
		if target.wildcardChild == nil {
			msg = fmt.Sprintf("目标节点的通配符子节点为空")
			return msg, false
		}
		_, equal := n.wildcardChild.equal(target.wildcardChild)
		if !equal {
			msg = fmt.Sprintf("期望节点 %s 的通配符子节点与目标节点 %s 的通配符子节点不等", n.path, target.path)
			return msg, false
		}
	}

	// step5. 比较2个节点的处理函数是否相同
	wantHandler := reflect.ValueOf(n.HandleFunc)
	targetHandler := reflect.ValueOf(target.HandleFunc)
	if wantHandler != targetHandler {
		msg = fmt.Sprintf("节点的处理函数不等, 期望节点 %s 的处理函数为: %v, 目标节点 %s 的处理函数为: %v", n.path, wantHandler, target.path, targetHandler)
		return msg, false
	}

	// step6. 比较2个节点的子节点是否相同
	for path, child := range n.children {
		// step6.1 比对2个节点的子节点的路径是否相同
		dstChild, exist := target.children[path]
		if !exist {
			msg = fmt.Sprintf("目标节点中不存在路径为: %s 的子节点", path)
			return msg, false
		}

		// step6.2 对路径相同的子节点递归比对
		msg, equal := child.equal(dstChild)
		if !equal {
			return msg, false
		}
	}

	return "", true
}
```

### 7.3 参数路由的校验

#### 7.3.1 实现校验逻辑

设计中是不准备支持同样的参数路由和通配符路由一起注册的(即`/user/*`和`/user/:id`).

所以从逻辑上来讲,只需要实现:**注册参数路由时检测通配符路由是否存在;注册通配符路由时检测参数路由是否存在.若对方存在,则不允许注册即可**.

`node.go`

```go
// findOrCreate 本方法用于根据给定的path值 在当前节点的子节点中查找path为给定path值的节点
// 找到则返回 未找到则创建
func (n *node) findOrCreate(segment string) *node {
	// 若路径以:开头 则查找或创建参数子节点
	if strings.HasPrefix(segment, ":") {
		if n.wildcardChild != nil {
			msg := fmt.Sprintf("web: 非法路由,节点 %s 已有通配符路由.不允许同时注册通配符路由和参数路由", n.path)
			panic(msg)
		}

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

	// 若路径为* 则查找或创建通配符子节点
	if segment == "*" {
		if n.paramChild != nil {
			msg := fmt.Sprintf("web: 非法路由,节点 %s 已有参数路由.不允许同时注册通配符路由和参数路由", n.path)
			panic(msg)
		}
		
		if n.wildcardChild == nil {
			n.wildcardChild = &node{
				path: "*",
			}
		}
		return n.wildcardChild
	}

	if n.children == nil {
		n.children = make(map[string]*node)
	}

	target, exist := n.children[segment]
	if !exist {
		// 当前节点的子节点映射中不存在目标子节点 则创建目标子节点 将子节点加入当前节点的子节点映射后返回
		target = &node{
			path: segment,
		}
		n.children[segment] = target
		return target
	}

	// 当前节点的子节点映射中存在目标子节点 则直接返回
	return target
}
```

#### 7.3.2 测试

`router_test.go`:

```go
func TestRouter_findRoute_param_and_wildcard_coexist(t *testing.T) {
	// step1. 注册通配符路由
	r := newRouter()
	mockHandleFunc := func(ctx *Context) {}
	r.addRoute(http.MethodGet, "/user/*", mockHandleFunc)

	// step2. 断言非法注册
	panicFunc := func() {
		r.addRoute(http.MethodGet, "/user/:id", mockHandleFunc)
	}

	assert.Panicsf(t, panicFunc, "web: 非法路由,节点 detail 已有通配符路由.不允许同时注册通配符路由和参数路由")
}
```

### 7.4 参数路由的查找

#### 7.4.1 编写测试用例

`router_test.go`

```go
func TestRouter_findParamRoute(t *testing.T) {
	// step1. 构造路由树
	testRoutes := []*TestNode{
		{
			method: http.MethodGet,
			path:   "/order/:id",
		},
		{
			method: http.MethodGet,
			path:   "/user/:id/detail",
		},
	}

	r := newRouter()
	mockHandle := func(ctx *Context) {}
	for _, testRoute := range testRoutes {
		r.addRoute(testRoute.method, testRoute.path, mockHandle)
	}

	// step2. 构造测试用例
	testCases := []struct {
		name     string
		method   string
		path     string
		isFound  bool
		wantNode *node
	}{
		{
			name:    "param route",
			method:  http.MethodGet,
			path:    "/order/5",
			isFound: true,
			wantNode: &node{
				path:          ":id",
				children:      nil,
				wildcardChild: nil,
				paramChild:    nil,
				HandleFunc:    mockHandle,
			},
		},
		{
			name:    "param route",
			method:  http.MethodGet,
			path:    "/user/1/detail",
			isFound: true,
			wantNode: &node{
				path:          "detail",
				children:      nil,
				wildcardChild: nil,
				paramChild:    nil,
				HandleFunc:    mockHandle,
			},
		},
	}

	for _, testCase := range testCases {
		t.Run(testCase.name, func(t *testing.T) {
			findNode, found := r.findRoute(testCase.method, testCase.path)
			assert.True(t, found, "节点未找到")
			if !found {
				return
			}
			msg, equal := testCase.wantNode.equal(findNode)
			assert.True(t, equal, msg)
		})
	}
}
```

此时运行测试用例报错节点未找到,因为此时没有查找参数子节点的逻辑

#### 7.4.2 实现参数路由的查找

`node.go`:

```go
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找path为给定path值的节点
// 找到则返回节点 否则返回 nil, false
func (n *node) childOf(path string) (*node, bool) {
	if n.children == nil {
		if n.paramChild != nil {
			return n.paramChild, true
		}

		return n.wildcardChild, n.wildcardChild != nil
	}

	child, found := n.children[path]
	if !found {
		if n.paramChild != nil {
			return n.paramChild, true
		}
		
		return n.wildcardChild, n.wildcardChild != nil
	}

	return child, true
}
```

此时测试用例即可通过

### 7.5 参数路由的参数值

#### 7.5.1 定义新类型

需要定义一个新的类型,该类型包含命中的节点,以及当该节点为参数路由节点时的参数名和参数值

`match_node.go`:

```go
package v4_rc

type matchNode struct {
	node       *node             // node 命中的节点
	pathParams map[string]string // pathParams 节点对应的路由参数 其中key为参数名 value为参数值
}
```

#### 7.5.2 修改`node.childOf()`方法

`node.childOf()`方法需要告知其调用者(也就是`router.findRoute()`方法)找到的节点是否为参数子节点.因为查找节点时,拿到的是参数值,而参数名是放在查找到的节点的path字段上的.

`node.go`:

```go
// childOf 本方法用于根据给定的path值 在当前节点的子节点映射中查找对应的子节点
// 若未在当前节点的子节点映射中查找到path对应的节点 则尝试查找当前节点的参数子节点
// 若未查找到当前节点的参数子节点 则尝试查找当前节点的通配符子节点
func (n *node) childOf(path string) (targetNode *node, isParamNode bool, isFound 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, true
}
```

#### 7.5.3 为`matchNode`结构体添加写入路由参数的方法

`match_node.go`:

```go
// addPathParam 用于添加路径参数
func (m *matchNode) addPathParam(key string, value string) {
	if m.pathParams == nil {
		m.pathParams = make(map[string]string)
	}
	m.pathParams[key] = value
}
```

#### 7.5.4 修改`router.findRoute()`方法

`router.findRoute()`方法则返回一个`*matchNode`类型的实例,其中包含了参数名和参数值(如果有的话).

`router.go`

```go
// findRoute 根据给定的HTTP动词和path 在路由树中查找匹配的节点
func (r *router) findRoute(method string, path string) (*matchNode, bool) {
	targetMatchNode := &matchNode{}
	// HTTP动词对应的路由树不存在 直接返回nil false即可
	root, ok := r.trees[method]
	if !ok {
		return nil, false
	}

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

	// 在路由树中逐层查找节点
	path = strings.TrimLeft(path, "/")
	pathSegments := strings.Split(path, "/")
	for _, pathSegment := range pathSegments {
		child, isParam, ok := target.childOf(pathSegment)
		if !ok {
			return nil, false
		}
		
		if isParam {
			key := strings.TrimPrefix(child.path, ":")
			value := pathSegment
			targetMatchNode.addPathParam(key, value)
		}
		
		target = child
	}

	targetMatchNode.node = target
	return targetMatchNode, true
}
```

#### 7.5.5 修改`Context`结构体

`context.go`

```go
// Context 路由处理函数的上下文
type Context struct {
	Req        *http.Request       // Req HTTP请求
	Resp       http.ResponseWriter // Resp HTTP响应
	PathParams map[string]string   // PathParams 参数路由的参数
}
```

#### 7.5.6 修改`HTTPServer.serve()`方法

`server.go`

```go
// serve 查找路由树并执行匹配到的节点所对应的处理函数
func (s *HTTPServer) serve(ctx *Context) {
	method := ctx.Req.Method
	path := ctx.Req.URL.Path
	targetNode, found := s.router.findRoute(method, path)
	if !found || targetNode.node.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		_, _ = ctx.Resp.Write([]byte("not found"))
		return
	}
	ctx.PathParams = targetNode.pathParams
	targetNode.node.HandleFunc(ctx)
}
```

#### 7.5.7 测试HTTPServer

`server_test.go`:

```go
func TestServer_Start(t *testing.T) {
	s := NewHTTPServer()

	wildcardHandleFunc := func(ctx *Context) {
		ctx.Resp.Write([]byte("hello order wildcard"))
	}
	s.addRoute(http.MethodGet, "/order/*", wildcardHandleFunc)

	handleFunc := func(ctx *Context) {
		ctx.Resp.Write([]byte("hello order detail"))
	}
	s.addRoute(http.MethodGet, "/order/detail", handleFunc)

	paramFunc := func(ctx *Context) {
		ctx.Resp.Write([]byte(fmt.Sprintf("%s", ctx.PathParams)))
	}
	s.addRoute(http.MethodGet, "/user/:id", paramFunc)

	err := s.Start(":8081")
	if err != nil {
		t.Fatal(err)
	}
}
```


# PART05.Context


# 5.01 Context-简介

![Context处理输入输出](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-594c9389a5c268e97dd520a405fd69e0234d0d5a%2FContext%E5%A4%84%E7%90%86%E8%BE%93%E5%85%A5%E8%BE%93%E5%87%BA.png?alt=media)

## PART1. HTTP语法简介

先来看一个最基本的HTTP HandleFunc示例:

```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", helloHandler)
	http.ListenAndServe(":8080", nil)
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, world!")
}
```

### 1.1 为什么有`http.Request`但是没有`http.Response`?

上文demo中的[`http.ResponseWriter`](https://github.com/golang/go/blob/master/src/net/http/server.go#L94),是一个接口.真正在运行时,这个`w`参数的类型是[`http.response`](https://github.com/golang/go/blob/master/src/net/http/server.go#L422),很明显这是一个私有的结构体,我们从外边是拿不到的.

接口[`http.ResponseWriter`](https://github.com/golang/go/blob/master/src/net/http/server.go#L94)实际上只有3个方法,看起来还是比较简单的:

```go
type ResponseWriter interface {
	Header() Header

	Write([]byte) (int, error)

	// WriteHeader 该方法用于写HTTP响应码
	WriteHeader(statusCode int)
}
```

### 1.2 `http.Request`

复杂的是[`http.Request`](https://github.com/golang/go/blob/master/src/net/http/request.go#L111),其方法如下两图示:

![http.Request的API-1](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request的API-1.png)

![http.Request的API-2](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request的API-2.png)

这些不用背,靠IDE的代码提示就行了.核心原则:**API不需要背的**.

#### 1.2.1 `http.Request.Body`属性

```go
package main

import (
	"fmt"
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/readBodyOnce", readBodyOnce)
	http.ListenAndServe(":8091", nil)
}

func readBodyOnce(w http.ResponseWriter, r *http.Request) {
	// 先读取一次body
	body, err := io.ReadAll(r.Body)
	if err != nil {
		fmt.Fprintf(w, "read body failed: %v", err)
		return
	}
	fmt.Fprintf(w, "read body: %s\n", string(body))

	// 再读取一次body
	body, err = io.ReadAll(r.Body)
	if err != nil {
		fmt.Fprintf(w, "read body one more time got error: %v", err)
		return
	}
	fmt.Fprintf(w, "read body one more time: [%s] and read body length %d \n", string(body), len(body))
}
```

![读取2次Request.Body](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/读取2次Request.Body.png)

注:此处老师上课说GET方法没有`http.Request.Body`,但实际上通过POSTMAN测试可以读取到,具体原因见附录.

通过这个demo我们可以看到:**`http.Request.Body`是只能读取1次的**.

[`http.Request.Body`](https://github.com/golang/go/blob/master/src/net/http/request.go#L186)的类型为[`io.ReadCloser`接口](https://github.com/golang/go/blob/master/src/io/io.go#L137):

该接口组合了`io.Reader`接口和`io.Closer`接口.

```go
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
	Reader
	// Closer 表示一个可关闭的资源
	Closer
}
```

在运行时,这个`http.Request.Body`的类型是[`http.body`](https://github.com/golang/go/blob/master/src/net/http/transfer.go#L803)(这个类型也是`io.Reader`接口的实现).真正实现读取是`http.body.Read()`方法.

这里多提一句,实际上完成读取的是`http.body.src.Read()`方法,而`http.body.src`真正的类型是`io.LimitedReader`,而`io.LimitedReader.R`的真正类型是`bufio.Reader`,这也就是为什么`http.Request.Body`只能被读取1次的原因.因为`bufio.Reader`的缓冲区中的数据一旦被读取,它就不会再存储在缓冲区中了,这意味着数据被消费了.

这种设计比较像JAVA中的`InputStream`和`OutputStream`.意思就是,你可以从中源源不断地读取,但你不能重复读取.

#### 1.2.2 `http.Request.GetBody`属性

```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/getBodyIsNil", getBodyIsNil)
	http.ListenAndServe(":8091", nil)
}

func getBodyIsNil(w http.ResponseWriter, r *http.Request) {
	if r.GetBody == nil {
		fmt.Fprintf(w, "GetBody is nil\n")
	} else {
		fmt.Fprintf(w, "GetBody is not nil\n")
	}
}
```

![http.Request.GetBody.png](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request.GetBody.png)

[`http.Request.GetBody`](https://github.com/golang/go/blob/master/src/net/http/request.go#L194)属性:**原则上可以多次读取**,但是在原生的`http.Request`中,其值恒为nil.所以有一些web框架在收到请求之后,第一件事就是给`http.Request.GetBody`赋值.注意其类型为`func() (io.ReadCloser, error)`.

因此在使用`http.Request.GetBody`之前,最好像demo中那样,先做一个判空,确保其在不为空的前提下再使用.

通常的使用就是将`http.Request.Body`中的内容读取出来之后,使用闭包写入到`http.Request.GetBody`中.

#### 1.2.3 `http.Request.URL.Query()`方法

```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/queryParams", queryParams)
	http.ListenAndServe(":8091", nil)
}

func queryParams(w http.ResponseWriter, r *http.Request) {
	values := r.URL.Query()
	fmt.Fprintf(w, "query is: %v\n", values)
}
```

![http.Request.URL.Query](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request.URL.Query.png)

注意,[`http.Request.URL.Query()`方法](https://github.com/golang/go/blob/master/src/net/url/url.go#L1121)的返回值类型为[`url.Values`](https://github.com/golang/go/blob/master/src/net/url/url.go#L886).

注意`url.Values`的类型,其key为string,value为\[]string,而非string.

再注意`url.Values.Get()`方法:

```go
func (v Values) Get(key string) string {
	if v == nil {
		return ""
	}
	vs := v[key]
	if len(vs) == 0 {
		return ""
	}
	return vs[0]
}
```

**注意方法中的`return vs[0]`,只是拿了同名参数的第1个值**.

还需要注意,所有的参数值都被解释为了string,所以还需要自己解析为其他类型.

#### 1.2.4 `http.Request.Header`属性

```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/header", header)
	http.ListenAndServe(":8091", nil)
}

func header(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "header is: %v\n", r.Header)
}
```

![http.Request.Header](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request.Header.png)

Header大体上分为2类,**一类是HTTP预定义的,另一类是自己定义的**.

GO会自动将Header的名字转换为**标准名字**,即大小写调整.

标准名字:每一个单词的首字母大写,单词与单词之间用`-`连接.

由[`CanonicalHeaderKey()`](https://github.com/golang/go/blob/master/src/net/http/header.go#L240)函数完成将请求头中的字段名转换为标准名称.

**通常用`X`开头来表明一个请求头字段是自定义的**,例如`X-My-Company-Your=header`

注意[`http.Request.Header`](https://github.com/golang/go/blob/master/src/net/http/header.go#L24)的类型,可以看到和`url.Values`的类型其实是相同的,都是`map[string][]string`.

所以实际上`url.Values`和`http.Request.Header`是可以互相转换的,只不过实操上很少这么干而已.

#### 1.2.4 `http.Request.Form`属性

```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/form", form)
	http.ListenAndServe(":8091", nil)
}

func form(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Before ParseForm: %v\n", r.Form)
	err := r.ParseForm()
	if err != nil {
		fmt.Fprintf(w, "ParseForm err: %v\n", err)
		return
	}
	fmt.Fprintf(w, "After ParseForm: %v\n", r.Form)
}
```

![http.Request.Form-表单参数](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request.Form-表单参数.png)

![http.Request.Form-请求头](https://github.com/step-by-step-wiki/GoBook/blob/main/img/Web框架之%20Server与路由树%20/Web框架之Context与AOP方案/1.Context-简介/http.Request.Form-请求头.png)

[`http.Request.Form`](https://github.com/golang/go/blob/master/src/net/http/request.go#L249)的类型同样为[`url.Values`](https://github.com/golang/go/blob/master/src/net/url/url.go#L886).

从demo中可以看到,使用`http.Request.Form`之前,需要先调用`http.Request.ParseForm()`方法.否则`http.Request.Form`值为nil.

另外,如果是表单提交,记得指定请求头中的`Content-Type`为`application/x-www-form-urlencoded`.

## 附录

[GET请求发送Body](https://github.com/yuzima/gate-of-babylon/blob/master/http/get_with_body.md)


# 5.02 Context-Beego Context设计分析

## PART1. Beego Context设计

### 1.1 `Context`

在之前的课程中说过,Beego的[Context](https://github.com/beego/beego/blob/develop/server/web/context/context.go#L72)中,既有`Input`又有`Request`;既有`Output`又有`ResponseWriter`.很明显功能上是重叠的,道理上来讲,`Input`和`Request`留一个即可;`Output`和`ResponseWriter`留一个即可;或者将`Request`放到`Input`中,将`ResponseWriter`放到`Output`中.

```go
type Context struct {
	Input          *BeegoInput
	Output         *BeegoOutput
	Request        *http.Request
	ResponseWriter *Response
	_xsrfToken     string
}
```

这样的设计,对于使用者而言,很难理解`Input`和`Request`、`Output`和`ResponseWriter`之间的区别和各自的使用场景.

其中:

* `Input`:对输入的封装
* `Output`:对输出的封装
* `ResponseWriter`:对响应的封装

### 1.2 `BeegoInput`

[BeegoInput](https://github.com/beego/beego/blob/develop/server/web/context/input.go#L46)是对输入的封装

```go
type BeegoInput struct {
	Context       *Context
	CruSession    session.Store
	pnames        []string
	pvalues       []string
	data          map[interface{}]interface{}
	dataLock      sync.RWMutex
	RequestBody   []byte
	RunMethod     string
	RunController reflect.Type
}
```

* `BeegoInput.Context`:反向引用了`Context`.而`Context`中也通过`Input`字段引用了`BeegoInput`,这种A引用B同时B引用A的场景在GO中是比较常见的设计
* `BeegoInput.CruSession`:耦合了Session
* `BeegoInput.pnames`:记录了参数路径的参数名
* `BeegoInput.pvalues`:记录了参数路径的参数值
* `BeegoInput.data`:记录了输入的数据(具体是啥我也不知道,没用过这个框架)
* `BeegoInput.RequestBody`:若将[`web.Config.CopyRequestBody`](https://github.com/beego/beego/blob/develop/server/web/config.go#L73)设置为true,则框架会将原生的`http.Request.Body`中的内容读到这个字段上

### 1.3 `BeegoOutput`

[BeegoOutput](https://github.com/beego/beego/blob/develop/server/web/context/output.go#L39)是对输出的封装

```go
// BeegoOutput does work for sending response header.
type BeegoOutput struct {
	Context    *Context
	Status     int
	EnableGzip bool
}
```

* `BeegoOutput.Context`:同样反向引用了`Context`
* `BeegoOutput.Status`:HTTP响应码
* `BeegoOutput.EnableGzip`:在写Response时是否启用Gzip压缩

### 1.4 `Response`

[`Response`](https://github.com/beego/beego/blob/develop/server/web/context/context.go#L334)

```go
type Response struct {
	http.ResponseWriter
	Started bool
	Status  int
	Elapsed time.Duration
}
```

* 组合了原生的`http.ResponseWriter`
* `Response.Started`:若该字段为true,则表示当前`Response`结构体实例已经被写入过了

## PART2. Beego 处理输入的方法

### 2.1 负责处理来自`http.Request.Body`输入的方法

#### 2.1.1 `Context`的Bind族方法

![Beego处理输入的方法-Bind族](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-a1fc5cbd30f4182721f218144cbe83280f080810%2FBeego%E5%A4%84%E7%90%86%E8%BE%93%E5%85%A5%E7%9A%84%E6%96%B9%E6%B3%95-Bind%E6%97%8F.png?alt=media)

`Context`中的`Bind`族方法,用于将不同形式的`Body`转化为具体的结构体.

#### 2.1.2 `Input`的`Bind()`方法

[`Input.Bind()`](https://github.com/beego/beego/blob/develop/server/web/context/input.go#L443)方法用于将输入的一部分(通过`key`参数指定)绑定到给定的`dest`上(这个`dest`可能是变量/map/结构体等).

### 2.2 负责处理来自其他部位的方法

![BeegoInput中负责从其他部位获取输入的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-746165312f54978b653eeaf0e7d7d871d980eabd%2FBeegoInput%E4%B8%AD%E8%B4%9F%E8%B4%A3%E4%BB%8E%E5%85%B6%E4%BB%96%E9%83%A8%E4%BD%8D%E8%8E%B7%E5%8F%96%E8%BE%93%E5%85%A5%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

这些方法负责从`http.Request`的其他部位(例如Header、查询参数等)上获取输入

![BeegoInput中负责从其他部位获取输入的方法-2](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-4e90a6dd84f13aea873526289d1ad3de524d75fa%2FBeegoInput%E4%B8%AD%E8%B4%9F%E8%B4%A3%E4%BB%8E%E5%85%B6%E4%BB%96%E9%83%A8%E4%BD%8D%E8%8E%B7%E5%8F%96%E8%BE%93%E5%85%A5%E7%9A%84%E6%96%B9%E6%B3%95-2.png?alt=media)

这里的`GetData()`中的data指的是其他Beego内部的其他组件或Middleware向`BeegoInput.data`中写入的数据

### 2.3 各种判断的方法

![Input中各种判断的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-15ff70d3b861085c4a5ea3687b788ff854f66aef%2FInput%E4%B8%AD%E5%90%84%E7%A7%8D%E5%88%A4%E6%96%AD%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

实际上这一类方法完全没必要提供,完全可以让用户自己判断,例如`if method == http.MethodGet`的方式去判断.

## PART3. Beego 处理输出的方法

### 3.1 将输入序列化之后输出的方法

![Context中处理序列化输出的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-ac715f19f3a673390d57d2f2fd59a592a91d8168%2FContext%E4%B8%AD%E5%A4%84%E7%90%86%E5%BA%8F%E5%88%97%E5%8C%96%E8%BE%93%E5%87%BA%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

这一类方法(Resp族的方法)将入参`data`转化成对应的格式,然后输出到响应中

### 3.2 渲染模板输出的方法

![Controller中渲染模板输出的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-815eb9b90944d46af39f629763dabd1634339d91%2FController%E4%B8%AD%E6%B8%B2%E6%9F%93%E6%A8%A1%E6%9D%BF%E8%BE%93%E5%87%BA%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

这一类方法是在`web.Controller`结构体上的,用于渲染页面并输出

### 3.3 输出各种格式数据的方法

![Output中定义输出各种格式数据的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-0fba5d39e2a7e5394abc6bf877ff2bd4a42eeac2%2FOutput%E4%B8%AD%E5%AE%9A%E4%B9%89%E8%BE%93%E5%87%BA%E5%90%84%E7%A7%8D%E6%A0%BC%E5%BC%8F%E6%95%B0%E6%8D%AE%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

这一类方法是在`context.BeegoOutput`结构体上的,实际上3.1小节中的Resp族的方法,最终调用的是`BeegoOutput`上对应数据格式的方法.

而且,`context.BeegoOutput`还有一些直接输出Body(`Body()`方法)和Header(`Header()`方法)的方法.

也就是说,写响应时,可以使用`Context`,也可以使用`Context.BeegoOutput`,看自己喜好即可.当然,这也会导致框架的使用者会有不同的风格来控制输出.


# 5.03 Context-Gin Context设计分析

## PART1. Gin Context设计

[`gin.Context`](https://github.com/gin-gonic/gin/blob/a481ee2897af1e368de5c919fbeb21b89aa26fc7/context.go#L51)也是放了一些和输入输出有关的字段.

```go
type Context struct {
	writermem responseWriter
	Request   *http.Request
	Writer    ResponseWriter

	Params   Params
	handlers HandlersChain
	index    int8
	fullPath string

	engine       *Engine
	params       *Params
	skippedNodes *[]skippedNode

	mu sync.RWMutex

	Keys map[string]any

	Errors errorMsgs

	Accepted []string

	queryCache url.Values

	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	sameSite http.SameSite
}
```

* `Context.Writer`:(其类型为[`gin.ResponseWriter`接口](https://github.com/gin-gonic/gin/blob/a481ee2897af1e368de5c919fbeb21b89aa26fc7/response_writer.go#L20),实际上是[`gin.responseWriter`类型](https://github.com/gin-gonic/gin/blob/a481ee2897af1e368de5c919fbeb21b89aa26fc7/response_writer.go#L46))封装了原生的`http.ResponseWriter`,但是使用了原生的`http.Request`.
* `Context.handlers`:责任链
* `Context.engine`:实现了HTTP Server
* `Context.formCache`:表单数据的缓存
* `Context.queryCache`:url参数的缓存
  * 这种缓存避免了重复读取和解析的开销
* `Context.sameSite`:其类型为[原生的`http.SameSite`类型](https://github.com/golang/go/blob/1cc19e5ba0a008df7baeb78e076e43f9d8e0abf2/src/net/http/cookie.go#L49),该字段的作用见附录.
  * 有一种观点认为,关于cookie的设置,既不应该在Context层面上设置,也不应该在Server层面上设置,而是应该让用户自行在输出cookie时设置.换言之,中间件不需要提供这个设置

```go
type SameSite int

const (
	SameSiteDefaultMode SameSite = iota + 1
	SameSiteLaxMode
	SameSiteStrictMode
	SameSiteNoneMode
)
```

## PART2. Gin 处理输入

### 2.1 从指定的key中读取数据的方法

![Context中根据Key读取数据的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-1a8dfa614608c556255c91774b59994eacc83388%2FContext%E4%B8%AD%E6%A0%B9%E6%8D%AEKey%E8%AF%BB%E5%8F%96%E6%95%B0%E6%8D%AE%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`gin.Context`中根据Key读取指定数据的方法

### 2.2 从不同部位读取数据的方法

![Context中从不同部位读取数据的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-41b0c260c58f77da5de24000de6ce61287dbdeee%2FContext%E4%B8%AD%E4%BB%8E%E4%B8%8D%E5%90%8C%E9%83%A8%E4%BD%8D%E8%AF%BB%E5%8F%96%E6%95%B0%E6%8D%AE%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`gin.Context`中从不同部位读取数据的方法

### 2.3 将输入转化为一个具象的结构体的方法

![Context中将输入转化为具象结构体的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-70f5bea73148876d864fa1a74d98ef5db521c757%2FContext%E4%B8%AD%E5%B0%86%E8%BE%93%E5%85%A5%E8%BD%AC%E5%8C%96%E4%B8%BA%E5%85%B7%E8%B1%A1%E7%BB%93%E6%9E%84%E4%BD%93%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`gin.Context`中将输入(整个body)转化为一个具体的对象的方法

## PART3. Gin 处理输出

### 3.1 返回具体格式响应的方法

![Context中返回具体格式响应的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-26e884eaeaa84a9d56fe0e5d8666e1ff3186360a%2FContext%E4%B8%AD%E8%BF%94%E5%9B%9E%E5%85%B7%E4%BD%93%E6%A0%BC%E5%BC%8F%E5%93%8D%E5%BA%94%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`gin.Context`中返回具体格式响应的方法

### 3.2 控制Handler调度的方法

`gin.Context`还控制了Handler的调度(想想第一周讲过的gin中的责任链),因此`gin.Context`中还有中断或继续后续Handler执行的方法

![Context中控制Handler调度的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-7e3881032958aa1c73bbdeb13b84185c371e82d4%2FContext%E4%B8%AD%E6%8E%A7%E5%88%B6Handler%E8%B0%83%E5%BA%A6%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

### 3.3 渲染页面的方法

![Context中渲染页面的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-7f47a0c53f41a3239d34877f851f37eec924f870%2FContext%E4%B8%AD%E6%B8%B2%E6%9F%93%E9%A1%B5%E9%9D%A2%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`gin.Context`中渲染页面的方法

## 附录

### 1. cookie的sameSite属性的作用

`SameSite`属性用于控制您的网站如何将cookie设置为与跨站点请求一起发送.它可以帮助减少CSRF(跨站请求伪造)攻击的风险,并且可以用于您的网站的cookie防御策略的一部分.`SameSite`属性可以有以下值:

* `Strict`:这个值会阻止cookie跟随跨站点请求被发送.这意味着如果用户在另一个网站上点击导向您网站的链接,cookie将不会被发送,这可能会阻止用户从外部网站登录或使用您网站的功能
* `Lax`:类似于`Strict`,但是当用户从另一个网站导航到您的网站时,例如通过点击一个链接,cookie会被发送.这提供了一个折中的方式,既提供了一定级别的CSRF保护,同时又允许一些跨站点请求,例如从其他网站的链接点击
* `None`:这个值会允许cookie在所有跨站点请求中发送.如果设置为`None`,必须同时将`Secure`属性设置为`true`,这样cookie只会通过安全的HTTPS连接发送

默认情况下,如果没有明确设置`SameSite`属性,大多数现代浏览器会将其视为`Lax`.这是为了提高网络的整体安全性,但开发者需要了解这一点,因为它可能会影响跨站点登录和集成的行为


# 5.04 Context-Echo和Iris的Context设计分析

## PART1. Echo Context设计分析

### 1.1 Echo Context的设计

Echo的[`Context`](https://github.com/labstack/echo/blob/584cb85a6b749846ac26a8cd151244ab281f2abc/context.go#L19)被设计为接口,这一点和Beego、Gin都不太一样,但实际上,它只有1个实现[`context`](https://github.com/labstack/echo/blob/584cb85a6b749846ac26a8cd151244ab281f2abc/context.go#L200).

这意味着其他人也可以提供`Context`接口的不同实现,但实际上也并没有什么人去实现它.

从方法定义上来看,Echo的`context`和Beego、Gin的都差不多,也是维护来自各部分的输入和输出.

```go
context struct {
	request  *http.Request
	response *Response
	path     string
	pnames   []string
	pvalues  []string
	query    url.Values
	handler  HandlerFunc
	store    Map
	echo     *Echo
	logger   Logger
	lock     sync.RWMutex
}
```

可以看到,和Gin的`Context`设计一样,Echo的`Context`也是自行封装了一个[Response](https://github.com/labstack/echo/blob/master/response.go#L13),但是使用了原生的`http.Request`.

* `context.logger`:这意味着Echo把日志输出定义在了context级别上.有一种观点认为日志输出应该定义在Server上,而非Context上
* `context.lock`:有一种观点认为Context没有必要实现线程安全.因为正常情况下1个请求打到Server端,就应该只有1个Context去处理这个请求(因为Server端的基本模型是每收到1个请求,就有1个goroutine去处理这个请求,因此应该只有1个Context).如果在中间件使用者定义的HandleFunc内部,中间件使用者自己又启动了一些goroutine,那么应该让中间件使用者自己去在他创建的这些goroutine之间确保这个Context的线程安全,而非由中间件研发者替他对Context的线程安全做出保证

### 1.2 Echo 处理输入

![EchoContext中处理各部分输入的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-9a7dad0913c182a057ba8612250adbedc0f998c4%2FEchoContext%E4%B8%AD%E5%A4%84%E7%90%86%E5%90%84%E9%83%A8%E5%88%86%E8%BE%93%E5%85%A5%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`echo.context`中从不同部位读取指定数据的方法

### 1.3 Echo 处理输出

![EchoContext中处理各种格式输出的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-b63a925f8ff8223235837c135508613c2e95edd2%2FEchoContext%E4%B8%AD%E5%A4%84%E7%90%86%E5%90%84%E7%A7%8D%E6%A0%BC%E5%BC%8F%E8%BE%93%E5%87%BA%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`echo.context`中处理各种格式输出的方法.包括渲染页面的方法(`context.Render`)

## PART2. Iris Context设计分析

### 2.1 Iris Context的设计

Iris的[`Context`](https://github.com/kataras/iris/blob/55357a125b16a8057496f4040b8b9ab0089d72d9/context/context.go#L91)也是设计为接口,也是有一个默认的实现[`Context`](https://github.com/kataras/iris/blob/v12/context/context.go#L1057)结构体.

这意味着其他人也可以提供`Context`接口的不同实现,但实际上也并没有什么人去实现它.

从抽象层级上来讲,确实这种设计比Beego和Gin的抽象层级更高.但在这个场景下的前提是:真的需要`Context`接口的多个实现.很明显这个场景下是没这个需求的.

注:截止编写笔记的日期(2023.11.15),Iris不再是这种设计,而是直接使用[`Context`](https://github.com/kataras/iris/blob/main/context/context.go#L131)结构体,该结构体实现了多个接口.

### 2.2 Iris 处理输入

![IrisContext中处理各部分输入的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-be023d7628cf1c6e0df71dc20f98e6be08bd9a4b%2FIrisContext%E4%B8%AD%E5%A4%84%E7%90%86%E5%90%84%E9%83%A8%E5%88%86%E8%BE%93%E5%85%A5%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`context.Context`中处理各部分输入的方法

### 2.3 Iris 处理输出

![IrisContext中处理各种格式输出的方法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-106c3079698f5a5d093a7d405b79df701771decd%2FIrisContext%E4%B8%AD%E5%A4%84%E7%90%86%E5%90%84%E7%A7%8D%E6%A0%BC%E5%BC%8F%E8%BE%93%E5%87%BA%E7%9A%84%E6%96%B9%E6%B3%95.png?alt=media)

`context.Context`中处理各种格式输出的方法


# 5.05 Context-处理输入输出总结

## PART1. Context 处理输入

处理输入要解决的问题:

* **反序列化输入**: 将Body字节流转换成一个具体的类型
  * 例如将请求体中的一个JSON转化为一个自定义的结构体实例
* **处理表单输入**: 可以看做是一个和JSON或者XML类似的、特殊的序列化方式
* **处理查询参数**: 从URL中的查询参数中读取值,并且转化为对应的类型
* **处理路径参数**: 读取路径参数的值,并转化为具体的类型
* **重复读取Body**: `http.Request.Body`默认只能读取1次,不能重复读取
  * 因此我们自己的WEB框架要考虑封装这个`http.Request.Body`,以支持重复读取
* **读取Header**: 从Header中读取特定的值,并转化为对应的类型
* **模糊读取**: 按照一定的顺序,尝试从Body、Header、路径参数或Cookie中读取值,并转化为特定的类型
  * 有一种观点认为,WEB框架不太应该支持这种功能.因为接口的设计者事前一定是知道一个请求参数到底从哪儿来的(比如他设计了一个RESTful风格的API,那么他的`get`接口就一定带有一个`id`之类的查询参数).如果中间件设计者提供了这种API,那么就意味着允许其使用者作出一个不良的实践.因此WEB框架不太应该支持这种功能.
  * **作为一个设计者(此处的设计者不特指API设计者还是中间件设计者),不要提供模糊的API,而是要提供清晰的API**
  * 在公司设计中间件时,更要注意这一点.因为之所以一帮人组了个团队去上班,除了把功能和页面做出来之外,更重要的是**规范**.如果你提供了这种模糊的API,实际上你就是在**无意间破坏了这种约定**.就像打魔兽世界,野团的raid效率通常是不如公会团的,原因就在于**规范**.

## PART2. Context 处理输出

处理输出要解决的问题:

* **序列化输出**: 按照某种特定的格式输出数据,例如JSON或XML
* **渲染页面**: 要考虑模板路径、命名和渲染的问题
* **处理状态码**: 允许用户返回特定状态码的响应,例如HTTP 404
* **错误页面**: 特定HTTP Status或error的时候,能够重定向到一个错误页面,例如404被重定向到首页
* **设置Cookie**: 设置Cookie的值
* **设置Header**: 往Header中写入一些内容


# 5.06 Context-处理输入之Body输入

本节课工程结构如下:

```
(base) yanglei@yuanhong 02-bindJSON % 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. Body输入JSON的反序列化

JSON作为最为常见的输入格式,可以率先支持.其余的类似于XML或者protobuf都可以按照类似的思路支持.

其实这里就是实现一个将请求体中的JSON反序列化到一个给定的结构体实例上,并没有什么复杂的逻辑.

`context.go`:

```go
package bindJSON

import (
	"encoding/json"
	"errors"
	"net/http"
)

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

// BindJSON 绑定请求体中的JSON到给定的实例(这里的实例不一定是结构体实例,还有可能是个map)上
func (c *Context) BindJSON(target any) error {
	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if c.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	decoder := json.NewDecoder(c.Req.Body)
	return decoder.Decode(target)
}
```

这里需要说明的是,也可以使用`json.Unmarshal()`来完成反序列化,但是相比于这个实现,多了一个步骤:需要将`http.Request.Body`使用`io.ReadAll()`将其内容读取为一个`[]byte`.因为`json.Unmarshal()`是不支持直接使用`io.Reader`接口的实现作为入参的.两种实现方式的比对如下:

```go
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
)

type User struct {
	Id int `json:"id"`
}

func main() {
	http.HandleFunc("/unmarshal", unmarshalHandle)
	http.HandleFunc("/decoder", decoderHandle)
	http.ListenAndServe(":8091", nil)
}

func unmarshalHandle(w http.ResponseWriter, r *http.Request) {
	byteSlice, _ := io.ReadAll(r.Body)
	err := json.Unmarshal(byteSlice, &User{})
	if err != nil {
		fmt.Fprintf(w, "decode failed: %v", err)
		return
	}

	afterRead, _ := io.ReadAll(r.Body)
	fmt.Fprintf(w, "after read: %s", string(afterRead))
}

func decoderHandle(w http.ResponseWriter, r *http.Request) {
	decoder := json.NewDecoder(r.Body)
	err := decoder.Decode(&User{})
	if err != nil {
		fmt.Fprintf(w, "decode failed: %v", err)
		return
	}

	afterRead, _ := io.ReadAll(r.Body)
	fmt.Fprintf(w, "after read: %s", string(afterRead))
}
```

很明显看到使用`json.Unmarshal()`反序列化的实现多了一个读取的步骤.而这是没有太大意义的.

## PART2. JSON输入控制选项

### 2.1 实现

在JSON的反序列化过程中,有2个选项(这2个选项的具体功能与示例见附录部分):

* `json.Decoder.UseNumber()`
* `json.Decoder.DisallowUnknownFields()`

那么问题来了,我们是否还要提供一个带有选项的JSON序列化方法(代码如下)?

```go
// BindJSONOpt 绑定请求体中的JSON到给定的实例(这里的实例不一定是结构体实例,还有可能是个map)上
// 同时支持指定是否使用Number类型,以及是否禁止未知字段
func (c *Context) BindJSONOpt(target any, useNumber bool, disallowUnknownFields bool) error {
	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if c.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	decoder := json.NewDecoder(c.Req.Body)

	if useNumber {
		decoder.UseNumber()
	}

	if disallowUnknownFields {
		decoder.DisallowUnknownFields()
	}

	return decoder.Decode(target)
}
```

### 2.2 使用者的需求

严谨地讲,如果用户真的有这种需求,那么他可能需要的是:

#### 2.2.1 控制整个应用

这种类似给整个框架提供一个配置.大致实现如下:

`config.go`:

```go
package bindJSON

type Config struct {
	// UseNumber 反序列化JSON时是否使用Number类型
	UseNumber bool
	// DisallowUnknownFields 反序列化JSON时是否禁止未知字段
	DisallowUnknownFields bool
}

```

#### 2.2.2 控制单一HTTPServer实例

这种实现只需在`HTTPServer`结构体上增加控制这2个选项的字段即可:

```go
package bindJSON

// HTTPServer HTTP服务器
type HTTPServer struct {
	router
	// useNumber 反序列化JSON时是否使用Number类型
	useNumber bool
	// disallowUnknownFields 反序列化JSON时是否禁止未知字段
	disallowUnknownFields bool
}
```

#### 2.2.3 控制特定路径

例如针对所有在`/user/`这个路径下进行的JSON反序列化操作,允许(拒绝)使用Number类型或允许(拒绝)出现结构体中未定义的字段

#### 2.2.4 控制特定路由

例如针对`/user/details`路由进行的JSON反序列化操作,允许(拒绝)使用Number类型或允许(拒绝)出现结构体中未定义的字段

### 2.3 结论

结论:**在反序列化JSON时,完全不需要提供支持控制`UseNumber()`和`DisallowUnknownFields()`的API**.

理由:对于绝大多数用户来说,他们不会尝试控制这2个选项.即使真的有这个需求,我们上述实现的`Context.BindJSON()`逻辑较为简单,且代码量不大,完全可以让框架的使用者照抄这个方法,然后自行实现一个功能和上文中实现的`Context.BindJSONOpt()`方法相同的方法.

如果`Context.BindJSON()`被设计为支持提供控制`UseNumber()`和`DisallowUnknownFields()`选项的方法,那么就意味着所有用户在调用时都需要传递`useNumber`和`disallowUnknownFields`这两个实参.而实际上还是刚才那句话:**对于绝大多数用户来说,他们不会尝试控制这2个选项**.

**记住,设计中间件时,要解决大部分人的需求.这里所谓的"大部分人的需求",其实就是根据自己的使用经验去猜测用户会如何使用自己的中间件,最终得出的一个结论**.

在设计API时,要控制住一个"度".换言之,如果用户有一些小众的需求,不是不能支持,而是要在**实现小众需求不影响核心**的前提下实现这些小众的需求.

更不能**让大多数人为小部分人付出代价**.因为有些小众的需求实现起来会非常耗时,实现这种需求的代码就不要放到主流程代码中,将这些实现小众需求的代码挪出来.或者说,**如果为了支持一个小众的需求,反而会影响到大部分主流用户的使用,那么就不要支持这个小众的需求**.

更进一步地讲:**如果一个小众需求,用户可以自己解决,那么就不要在框架核心上支持.要克制自己!**

## 附录

### 附录1:`json.Decoder.UseNumber()`

#### 1.1 功能说明

在Go语言中的`encoding/json`包中,`json.Decoder`类型的`UseNumber()`方法是用来指导`Decoder`在解码JSON数据时如何处理数字.默认情况下,当`Decoder`遇到一个数字时,它会将该数字解码为`float64`.但是,如果`UseNumber()`被调用,`Decoder`将代替将数字解码为[`json.Number`](https://github.com/golang/go/blob/master/src/encoding/json/decode.go#L189)类型.

`json.Number`是一个字符串类型,这意味着数字在解码过程中不会失去精度.这在处理大整数和精确的小数点数值时特别有用,因为直接解码为`float64`可能会因为精度限制而丢失信息.例如,一个非常大的整数可能比`float64`能精确表示的最大整数还要大,或者一个小数可能需要比`float64`能提供的精度更高的精度.

当使用`json.Number`时,你可以稍后将这个值转换为你想要的确切数字类型,如`int64`、`float64`或者你自己的自定义数字类型,这样可以确保在转换过程中控制精度和范围.

简单来说,`UseNumber()`允许你更灵活和精确地处理JSON中的数字,防止在解码过程中出现不必要的精度损失.

#### 1.2 示例

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"math/big"
)

var (
	jsonBlob = []byte(`{"int_max":9223372036854775807}`)
)

func main() {
	noUseNumber()
	useNumber()
}

// noUseNumber 不使用`UseNumber`选项进行JSON反序列化
func noUseNumber() {
	data := make(map[string]any)

	if err := json.Unmarshal(jsonBlob, &data); err != nil {
		log.Fatal(err)
	}

	// 输出一个浮点数 这个浮点数可能会出现精度丢失的现象
	fmt.Println(data["int_max"])
}

// useNumber 使用`UseNumber`选项进行JSON反序列化
func useNumber() {
	data := make(map[string]json.Number)

	decoder := json.NewDecoder(bytes.NewReader(jsonBlob))
	decoder.UseNumber()

	if err := decoder.Decode(&data); err != nil {
		log.Fatal(err)
	}

	fmt.Println(data["int_max"])

	// 将这个json.Number类型的值安全的转换为更精确的数字类型
	intValue, ok := new(big.Int).SetString(data["int_max"].String(), 10)
	if !ok {
		log.Fatal("Big int conversion failed")
	}

	// 输出一个精确的大整数
	fmt.Printf("The big int is: %d\n", intValue)
}
```

运行结果:

```
(base) yanglei@yuanhong 8-useNumber % go run useNumber.go 
9.223372036854776e+18
9223372036854775807
The big int is: 9223372036854775807
```

### 附录2:`json.Decoder.DisallowUnknownFields()`

#### 2.1 功能说明

在Go语言中,`json.Decoder`的`DisallowUnknownFields()`方法的作用是设置`Decoder`在解码JSON数据时,不允许出现结构体中未定义的字段.如果设置了这个方法,当`Decoder`遇到结构体中没有定义的字段时,它将返回一个错误.

这个方法对于确保JSON数据的格式严格符合预期的结构体非常有用,它可以防止因为JSON中的意外字段而导致的潜在错误,并确保数据的解码不会静默忽略任何字段.

#### 2.2 示例

```go
package main

import (
	"encoding/json"
	"log"
	"strings"
)

var jsonStr = `{"knownField":"value", "unknownField":"should cause error"}`

type MyStruct struct {
	KnownField string `json:"knownField"`
}

func main() {
	noDisallowUnknownFields()
	disallowUnknownFields()
}

// noDisallowUnknownFields 不使用`DisallowUnknownFields`选项进行JSON反序列化
func noDisallowUnknownFields() {
	myStruct := &MyStruct{}
	err := json.Unmarshal([]byte(jsonStr), myStruct)
	if err != nil {
		log.Fatal("Unmarshal error:", err)
	}

	log.Printf("Unmarshal success: %+v\n", myStruct)
}

// disallowUnknownFields 使用`DisallowUnknownFields`选项进行JSON反序列化
func disallowUnknownFields() {
	myStruct := &MyStruct{}
	decoder := json.NewDecoder(strings.NewReader(jsonStr))
	decoder.DisallowUnknownFields()
	err := decoder.Decode(myStruct)
	if err != nil {
		// 这里将输出错误 因为JSON中包含了MyStruct没有定义的unknownField
		log.Fatal("Decode error:", err)
	}

	log.Printf("Decode success: %+v\n", myStruct)
}
```

运行结果:

```
(base) yanglei@yuanhong 9-disallowUnknownFields % go run disallowUnknownFields.go 
2023/11/16 00:19:54 Unmarshal success: &{KnownField:value}
2023/11/16 00:19:54 Decode error:json: unknown field "unknownField"
exit status 1
```

在这个例子中,尝试解码包含未知字段`unknownField`的JSON字符串将会失败,并返回一个错误,因为`MyStruct`结构体中只定义了`KnownField`字段.如果你没有调用`DisallowUnknownFields()`,则未知字段会被解码过程中忽略掉,并且不会报错.


# 5.07 Context-处理输入之表单输入

本节课工程路径如下:

```
(base) yanglei@yuanhong 03-formValue % 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
```

注:相比于`02-bindJSON`,有如下变更:

1. 删除了演示用的`config.go`
2. 删除了`HTTPServer`结构体中的`useNumber`和`disallowUnknownFields`字段(这两个字段也是上节课演示用的)

## PART1. `http.Request.Form`与`http.Request.PostForm`的区别

* `http.Request.Form`:可以接收URL中的参数以及HTTP动词为`PATCH`、`POST`、`PUT`时的表单数据
* `http.Request.PostForm`:只能接收HTTP动词为`PATCH`、`POST`、`PUT`时的表单数据
* **不管使用以上二者中的哪一种,都要先调用`http.Request.ParseForm()`解析表单数据后,这两个字段才有数据**

示例:

```go
package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/formAndPostForm", formAndPostFormHandle)
	log.Fatal(http.ListenAndServe(":8091", nil))
}

func formAndPostFormHandle(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		log.Fatal("ParseForm failed: ", err)
	}

	fmt.Fprintf(w, "Form: %v\n", r.Form)
	fmt.Fprintf(w, "PostForm: %v\n", r.PostForm)
}
```

请求如下图示:

![url参数与表单参数](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-10315a5f2e5afe037cf9a15e49d15267d0387fef%2Furl%E5%8F%82%E6%95%B0%E4%B8%8E%E8%A1%A8%E5%8D%95%E5%8F%82%E6%95%B0.png?alt=media)

可以看到,URL中的参数为:

* `name=zhangsan`
* `age=18`

表单数据中的参数为:

* `name=lisi`
* `gender=male`

响应如下图示:

![url参数与表单参数的响应](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-437df71a01860901c52c7bef5cc23a6d52767bf8%2Furl%E5%8F%82%E6%95%B0%E4%B8%8E%E8%A1%A8%E5%8D%95%E5%8F%82%E6%95%B0%E7%9A%84%E5%93%8D%E5%BA%94.png?alt=media)

可以看到:

* `http.Request.Form`既可以收到URL中的参数,也可以接收到表单中的参数
* `http.Request.PostForm`只能接收到表单中的参数
* **注意,二者的返回值类型都是**[**`url.Values`**](https://github.com/golang/go/blob/d6ef98b8fa4851f025779ef4ade084d63290de2a/src/net/url/url.go#L886)**,实际上就是`map[string][]string`**

二者最核心的区别在于:

* `http.Request.Form`:基本上可以认为**能够拿到所有的表单数据**
* `http.Request.PostForm`:只能拿到ContentType为`x-www-form-urlencoded`时的表单数据

**不建议使用表单的方式进行通信,而是建议在Body中写入JSON或Protobuf的方式进行通信.因为在数据结构较为复杂的场景下,使用使用JSON或Protobuf比使用Form的可读性要更高.因此在可选择的前提下,尽可能采用Body中中写入JSON或Protobuf的方式进行通信**.

## PART2. 实现`Context.FormValue()`方法

### 2.1 完全自行实现

```go
// FormValue1 获取表单中给定键的值
func (c *Context) FormValue1(key string) (value string, err error) {
	err = c.Req.ParseForm()
	if err != nil {
		return "", errors.New("web绑定错误: 解析表单失败: " + err.Error())
	}

	values, ok := c.Req.Form[key]
	if !ok {
		return "", errors.New("web绑定错误: 表单中不存在键: " + key)
	}

	// Tips: 这里只返回第一个值,这样的设计是参照了net/http包中的FormValue()方法
	return values[0], nil
}
```

注意,这里只返回同名参数中的第1个值,是参照了[`http.Request.FormValue()`](https://github.com/golang/go/blob/master/src/net/http/request.go#L1378)方法的返回.

这里不建议如下方式的实现:

```go
// FormValue2 获取表单中给定键的值 不建议按此方式返回 因为大部分场景下表单中的键都是唯一的
func (c *Context) FormValue2(key string) (values []string, err error) {
	err = c.Req.ParseForm()
	if err != nil {
		return nil, errors.New("web绑定错误: 解析表单失败: " + err.Error())
	}

	values, ok := c.Req.Form[key]
	if !ok {
		return nil, errors.New("web绑定错误: 表单中不存在键: " + key)
	}

	return values, nil
}
```

**不建议按此方式返回,因为大部分场景下表单中的键都是唯一的**.还是上节课提到的观点:**为了支持一个小众的需求,反而会影响到大部分主流用户的使用,那么就不要支持这个小众的需求**.

### 2.2 依赖原生API的实现

```go
// FormValue3 获取表单中给定键的值 推荐使用这种实现 因为这种实现的语义和原生API语义相同
func (c *Context) FormValue3(key string) (value string, err error) {
	err = c.Req.ParseForm()
	if err != nil {
		return "", errors.New("web绑定错误: 解析表单失败: " + err.Error())
	}

	return c.Req.FormValue(key), nil
}
```

**推荐使用这种实现,因为这种实现的语义和原生API语义相同**.

甚至可以不要前边对于`http.Request.ParseForm()`方法返回值的错误判断部分.但不建议这么做.因为虽然[`http.Request.ParseMultipartForm()`](https://github.com/golang/go/blob/master/src/net/http/request.go#L1328)方法会进行解析并返回解析错误,但它返回的错误被[`http.Request.FormValue()`](https://github.com/golang/go/blob/master/src/net/http/request.go#L1390)方法在调用时给丢弃掉了,没有处理.所以最好还是主动自己解析一下,以防万一.

## PART3. 相关问题

### 3.1 多次调用`Context.FormValue()`方法是否会造成重复解析?

答案是不会的.**因为`http.Request.ParseForm()`方法的调用是幂等的**.证据如下:

* [`http.Request.ParseForm()`方法的注释说明](https://github.com/golang/go/blob/master/src/net/http/request.go#L1284)
* [仅在`http.Request.PostForm`字段值不为`nil`时解析](https://github.com/golang/go/blob/master/src/net/http/request.go#L1287)
* [仅在`http.Request.Form`字段值不为`nil`时解析](https://github.com/golang/go/blob/master/src/net/http/request.go#L1295)

这里有一种观点认为在GIN中,[`gin.Context.formCache`](https://github.com/gin-gonic/gin/blob/master/context.go#L82)字段是没有必要设计的,因为原生的`http.Request.Form`和`http.Request.PostForm`字段本身就起到了缓存的效果

### 3.2 是否需要提供返回其他数据类型的API?

例如:根据给定的`key`,返回一个`int64`类型数据的方法.其实现如下:

```go
// FormValueAsInt64 获取表单中给定键的值 并将该值转换为int64类型返回
func (c *Context) FormValueAsInt64(key string) (int64Value int64, err error) {
	err = c.Req.ParseForm()
	if err != nil {
		return 0, errors.New("web绑定错误: 解析表单失败: " + err.Error())
	}

	value := c.Req.FormValue(key)
	return strconv.ParseInt(value, 10, 64)
}
```

那么问题来了:要不要实现这种(注意是这种不是这个)API?

如果要提供的话,至少要提供3个:

* 获取表单中给定键的值,并将该值转换为`int64`类型返回
* 获取表单中给定键的值,并将该值转换为`uint64`类型返回
* 获取表单中给定键的值,并将该值转换为`float64`类型返回

用户拿到这3种类型的数据,再自己向下转型(强制类型转换).

但其实不需要提供.这里因为其他处理输入的API也会遇到类似的问题,后边统一讲这个事情.


# 5.08 Context-处理输入之查询参数、路径参数和StringValue

## PART1. 处理查询参数

本部分工程结构如下:

```
(base) yanglei@bogon 04-queryValue % tree ./
./
├── context.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 11 files
```

查询参数:即**就是指在URL问号之后的部分**.

例如:`http://localhost:8081/form?name=xiaoming&age=18`.那么查询参数即为:

* `name=xiaoming`
* `age=18`

在之前处理表单处理的课程中我们可知,调用`http.Request.ParseForm()`方法,可以找到这部分参数值.

乍一看这个需求实现起来还是比较简单的:

```go
// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	return c.Req.URL.Query().Get(key), nil
}
```

### 1.1 问题1:多次调用是否会重复解析?

[`http.Request.URL.Query()`](https://github.com/golang/go/blob/master/src/net/url/url.go#L1121)方法调用了[`url.ParseQuery()`](https://github.com/golang/go/blob/master/src/net/url/url.go#L933)函数,而[`url.ParseQuery()`](https://github.com/golang/go/blob/master/src/net/url/url.go#L933)函数又调用了[`url.parseQuery()`](https://github.com/golang/go/blob/master/src/net/url/url.go#L939)函数.实际上真正实现解析查询参数的正是这个[`url.parseQuery()`](https://github.com/golang/go/blob/master/src/net/url/url.go#L939)函数.**换言之,我们每一次调用`http.Ruquest.URL.Query()`方法,都要重新解析一次.因此我们要考虑把查询参数缓存起来**.

```go
package queryValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	return c.queryValues.Get(key), nil
}
```

注:

1. 此处删除了和本节课无关的代码
2. 这里的设计类似GIN框架,引入了一个查询参数缓存.**这个缓存是不存在所谓的缓存失效或缓存不一致问题的,因为对于Web框架而言,请求收到之后,请求参数就是确切无疑不会再变的**

### 1.2 问题2:无法区分给定的key到底是不存在,还是存在但值为空?

但是这个实现有一个问题:无法区分给定的key到底是不存在,还是存在但值为空

举例说明:

```go
package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/queryValue", queryValue)
	http.ListenAndServe(":8091", nil)
}

func queryValue(w http.ResponseWriter, r *http.Request) {
	// 获取查询字符串中的name参数
	name := r.URL.Query().Get("name")
	fmt.Fprintf(w, "name %s", name)
}
```

请求URL为`127.0.0.1:8091/queryValue`时,结果如下:

![无查询参数](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-11e01171f6d075f5667d91609c96663123e6d203%2F%E6%97%A0%E6%9F%A5%E8%AF%A2%E5%8F%82%E6%95%B0.png?alt=media)

请求URL为`127.0.0.1:8091/queryValue?name=`时,结果如下:

![有查询参数但值为空](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-ac1e8a2c7f90bdccebbb84fdd3b5c848402b303d%2F%E6%9C%89%E6%9F%A5%E8%AF%A2%E5%8F%82%E6%95%B0%E4%BD%86%E5%80%BC%E4%B8%BA%E7%A9%BA.png?alt=media)

再次提醒,[`url.Values`](https://github.com/golang/go/blob/master/src/net/url/url.go#L886)类型的本质是`map[string][]string`.因此只需要对我们的`Context.queryValues`做一个判断取值是否成功的操作,即可判断出到底是key到底是不存在,还是存在但值为空.

```go
package queryValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	values, ok := c.queryValues[key]
	if !ok {
		return "", errors.New("web绑定错误: 查询参数中不存在键: " + key)
	}

	return values[0], nil
}
```

这里可能会有人问:为什么访问这个map之前不需要判空?

这是因为在[`url.ParseQuery()`](https://github.com/golang/go/blob/master/src/net/url/url.go#L934)函数中,已经对返回值进行了初始化,因此上述代码中拿到的`c.queryValues`必然不为空,就算没有任何查询参数,其值也是一个**长度为0的`map[string][]string`**.

如果再严格一些,可以再加一个对`c.queryValues`长度是否为0的判断,以便判断是否存在查询参数:

```go
package queryValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	if len(c.queryValues) == 0 {
		return "", errors.New("web绑定错误: 无任何查询参数")
	}

	values, ok := c.queryValues[key]
	if !ok {
		return "", errors.New("web绑定错误: 查询参数中不存在键: " + key)
	}

	return values[0], nil
}
```

### 1.3 问题3:是否需要提供返回其他数据类型的API?

对于查询参数的读取,也面临着和读取表单参数一样的问题:是否需要提供返回其他数据类型的API?

例如:

```go
package queryValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	if len(c.queryValues) == 0 {
		return "", errors.New("web绑定错误: 无任何查询参数")
	}

	values, ok := c.queryValues[key]
	if !ok {
		return "", errors.New("web绑定错误: 查询参数中不存在键: " + key)
	}

	return values[0], nil
}

// QueryValueAsInt64 获取查询字符串中给定键的值并返回其int64表示
func (c *Context) QueryValueAsInt64(key string) (intValue int64, err error) {
	value, err := c.QueryValue(key)
	if err != nil {
		return 0, err
	}
	return strconv.ParseInt(value, 10, 64)
}
```

同样问题:如果需要提供,那就还要提供很多其他数据类型的API.

这里的答案同样是不需要的.后续我们统一解决这个问题.

## PART2. 处理路径参数

本章工程结构如下:

```
(base) yanglei@bogon 05-pathValue % tree ./
./
├── context.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 11 files
```

这里基本上和前面的表单、查询参数没太大的区别

```go
package pathValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// PathValue 获取路径参数中给定键的值
func (c *Context) PathValue(key string) (value string, err error) {
	if c.PathParams == nil {
		return "", errors.New("web绑定错误: 无任何路径参数")
	}

	value, ok := c.PathParams[key]
	if !ok {
		return "", errors.New("web绑定错误: 路径参数中不存在键: " + key)
	}

	return value, nil
}
```

注:

1. 此处删除了和本节课无关的代码

### 2.1 问题1:是否需要提供返回其他数据类型的API?

对于路径参数的读取,也面临着和查询参数、读取表单参数一样的问题:是否需要提供返回其他数据类型的API?

例如:

```go
package pathValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// PathValue 获取路径参数中给定键的值
func (c *Context) PathValue(key string) (value string, err error) {
	if c.PathParams == nil {
		return "", errors.New("web绑定错误: 无任何路径参数")
	}

	value, ok := c.PathParams[key]
	if !ok {
		return "", errors.New("web绑定错误: 路径参数中不存在键: " + key)
	}

	return value, nil
}

// PathValueAsInt64 获取路径参数中给定键的值并返回其int64表示
func (c *Context) PathValueAsInt64(key string) (intValue int64, err error) {
	value, err := c.PathValue(key)
	if err != nil {
		return 0, err
	}

	return strconv.ParseInt(value, 10, 64)
}
```

## PART3. 返回不同数据类型的输入

本章工程结构如下:

```
(base) yanglei@bogon 06-stringValue % tree ./
./
├── context.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
└── serverInterface.go

0 directories, 11 files
```

### 3.1 定义用于接收来自`Context`输入的类型

新建文件`stringValue.go`:

```go
package stringValue

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}
```

整体的思路就是:**`Context`中处理各部分输入的方法,不再返回`(string, err)`,而是返回一个`StringValue`类型的实例(注意不是指针,因为我们认为这个实例不需要被修改).后续所有类型转换的方法,均在`StringValue`类型上实现.换言之,`StringValue`类型的职责为:将各部分输入的值转换为对应的类型**.

### 3.2 修改`Context`中处理各部分输入的方法的返回值

此处各方法不再返回`(string, err)`,而是返回一个`StringValue`类型的实例.

#### 3.2.1 `Context.FormValue()`方法

```go
package stringValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// FormValue 获取表单中给定键的值
func (c *Context) FormValue(key string) (stringValue StringValue) {
	err := c.Req.ParseForm()
	if err != nil {
		return StringValue{err: err}
	}

	return StringValue{value: c.Req.FormValue(key)}
}
```

### 3.2.2 `Context.QueryValue()`方法

```go
package stringValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// QueryValue 获取查询字符串中给定键的值
func (c *Context) QueryValue(key string) (stringValue StringValue) {
	if c.queryValues == nil {
		c.queryValues = c.Req.URL.Query()
	}

	if len(c.queryValues) == 0 {
		return StringValue{err: errors.New("web绑定错误: 无任何查询参数")}
	}

	values, ok := c.queryValues[key]
	if !ok {
		return StringValue{err: errors.New("web绑定错误: 查询参数中不存在键: " + key)}
	}

	return StringValue{value: values[0]}
}
```

### 3.2.3 `Context.PathValue()`方法

```go
package stringValue

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// PathValue 获取路径参数中给定键的值
func (c *Context) PathValue(key string) (stringValue StringValue) {
	if c.PathParams == nil {
		return StringValue{err: errors.New("web绑定错误: 无任何路径参数")}
	}

	value, ok := c.PathParams[key]
	if !ok {
		return StringValue{err: errors.New("web绑定错误: 路径参数中不存在键: " + key)}
	}

	return StringValue{value: value}
}
```

### 3.3 新增将输入参数转换为对应类型表达的方法

#### 3.3.1 `StringValue.AsInt64()`

```go
package stringValue

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsInt64 将承载的值转换为int64类型
func (s StringValue) AsInt64() (value int64, err error) {
	if s.err != nil {
		return 0, s.err
	}

	return strconv.ParseInt(s.value, 10, 64)
}
```

#### 3.3.2 `StringValue.AsUint64()`

```go
package stringValue

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsUint64 将承载的值转换为uint64类型
func (s StringValue) AsUint64() (value uint64, err error) {
	if s.err != nil {
		return 0, s.err
	}

	return strconv.ParseUint(s.value, 10, 64)
}
```

### 3.3.3 `StringValue.AsFloat64()`

```go
package stringValue

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsFloat64 将承载的值转换为float64类型表示
func (s StringValue) AsFloat64() (value float64, err error) {
	if s.err != nil {
		return 0, s.err
	}

	return strconv.ParseFloat(s.value, 64)
}
```

### 3.4 在handleFunc中使用

新建文件`context_test.go`:

```go
package stringValue

import (
	"fmt"
	"testing"
)

func TestContext_PathValue(t *testing.T) {
	s := &HTTPServer{router: newRouter()}

	handleFunc := func(ctx *Context) {
		// 获取路径参数
		id, err := ctx.PathValue("id").AsInt64()
		if err != nil {
			ctx.Resp.WriteHeader(400)
			ctx.Resp.Write([]byte("id输入不正确: " + err.Error()))
			return
		}

		ctx.Resp.Write([]byte(fmt.Sprintf("hello %d", id)))
	}

	s.GET("/order/:id", handleFunc)
	_ = s.Start(":8091")
}
```

尝试请求:

![正确的请求参数](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e8400418dd7d950229248a86c75adba4d9a7d55c%2F%E6%AD%A3%E7%A1%AE%E7%9A%84%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0.png?alt=media)

![错误的请求参数](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3588b13d363c6581680d4316b0d3077908ffb804%2F%E9%94%99%E8%AF%AF%E7%9A%84%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0.png?alt=media)

这样设计的好处在于这一行代码:`ctx.PathValue("id").AsInt64()`

这种链式调用,有2个优点:

* 对于框架使用者而言,是非常方便的
* 对于框架设计者而言,每个类的职责非常清晰,保持`Context`的简约

这种设计思路来源于GO原生的`database/sql`包中的[`sql.Row`](https://github.com/golang/go/blob/master/src/database/sql/sql.go#L3431)结构体.可以看到[`sql.Row.Scan()`](https://github.com/golang/go/blob/master/src/database/sql/sql.go#L3443)方法,思路和我们设计`StringValue`时的将输入参数转换为对应类型表达的方法是相同的.

**同时,由于返回的都是结构体,因此可以保证在大多数的情况下,不会发生内存逃逸**


# 5.09 Context-处理输出

本节课工程结构如下:

```
(base) yanglei@yuanhong 07-respJSON % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
├── serverInterface.go
└── stringValue.go

0 directories, 13 files
```

## PART1. JSON响应

### 1.1 基本实现

这个需求还是比较简单的.其实只有2个步骤:

* step1. 调用`json.Marshal()`,拿到JSON反序列化后的字节切片
* step2. 将反序列化后的字节切片写入到响应(`http.ResponseWriter`,实际上是`http.response`)中

```go
package respJSON

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// RespJSONBase 以JSON格式输出相应
func (c *Context) RespJSONBase(obj any) (err error) {
	data, err := json.Marshal(obj)
	if err != nil {
		return err
	}

	_, err = c.Resp.Write(data)
	return err
}
```

### 1.2 进阶实现

更进一步的,这个方法应该完善的功能点有:

* 设置响应码
* 设置响应头中的`Content-Type`为`application/json`
* 设置响应头中的`Content-Length`为字节切片的长度
* 判断写入响应体的长度和JSON反序列化后的字节切片长度是否相同

```go
package respJSON

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// RespJSON 以JSON格式输出相应
func (c *Context) RespJSON(status int, obj any) (err error) {
	data, err := json.Marshal(obj)
	if err != nil {
		return err
	}

	c.Resp.Header().Set("Content-Type", "application/json")
	c.Resp.Header().Set("Content-Length", strconv.Itoa(len(data)))
	// Tips: 在写入响应状态码之前设置响应头 因为一旦调用了WriteHeader方法
	// Tips: 随后对响应头的任何修改都不会生效 因为响应头已经发送给客户端了
	c.Resp.WriteHeader(status)
	
	n, err := c.Resp.Write(data)
	if n != len(data) {
		return errors.New("web绑定错误: 写入响应体不完整")
	}

	return err
}
```

这里需要注意的点是:

**需要在写入响应状态码之前设置响应头**.一旦调用了`http.response.WriteHeader()`方法,随后对响应头的任何修改都不会生效,因为响应头已经发送给客户端了.因此需要先设置好所有的响应头,然后再调用`http.response.WriteHeader()`写入状态码

测试用例`context_test.go`如下:

```go
package respJSON

import (
	"testing"
)

func TestContext_RespJSON(t *testing.T) {
	s := &HTTPServer{router: newRouter()}

	handleFunc := func(ctx *Context) {
		type User struct {
			Name string `json:"name"`
		}

		// 获取路径参数
		id := ctx.PathValue("name")

		user := &User{Name: id.value}

		ctx.RespJSON(202, user)
	}

	s.GET("/user/:name", handleFunc)
	_ = s.Start(":8091")
}
```

![RespJSON响应体](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-196305a078b03d34efd76197efc74805948fb75d%2FRespJSON%E5%93%8D%E5%BA%94%E4%BD%93.png?alt=media)

![RespJSON响应头](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d67518fb938d052d85912520facd27eb33cd5bc8%2FRespJSON%E5%93%8D%E5%BA%94%E5%A4%B4.png?alt=media)

### 1.3 基于进阶实现的封装

例如可以封装一个返回HTTP状态码为200的JSON响应

```go
package respJSON

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// RespJSONOK 以JSON格式输出一个状态码为200的响应
func (c *Context) RespJSONOK(obj any) (err error) {
	return c.RespJSON(http.StatusOK, obj)
}
```

这里有一个问题:如果`obj`的类型为`string`或者`[]byte`,这种场景下用户该怎么办?

答:在这种场景下,用户已经不需要调用`RespJSON()`方法了,直接操作`http.ResponseWriter`即可

## PART2. 输出Cookie

本部分工程结构如下:

```
(base) yanglei@bogon 08-setCookie % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
├── serverInterface.go
└── stringValue.go

0 directories, 13 files
```

### 2.1 功能实现

这个功能实现起来也比较容易:

```go
package setCookie

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

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

// SetCookie 设置响应头中的Set-Cookie字段
func (c *Context) SetCookie(cookie *http.Cookie) {
	http.SetCookie(c.Resp, cookie)
}
```

在早期的GO中,是没有`http.SetCookie()`函数的.但实际上[`http.SetCookie()`](https://github.com/golang/go/blob/master/src/net/http/cookie.go#L169)函数的实现也是拼接一个`Set-Cookie`的响应头字段到响应头中.因此早期的GO WEB框架帮助框架使用者拼接了这个`Set-Cookie`字段.拼接的过程实际上就相当于实现[`http.Cookie.String()`](https://github.com/golang/go/blob/master/src/net/http/cookie.go#L179)方法.

### 2.2 问题1:是否应该在Context上提供一些关于cookie的默认配置?

例如配置同源策略:

```go
package setCookie

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
	// QueryValues 查询参数名值对
	queryValues url.Values
	// cookieSameSite cookie的SameSite属性 即同源策略
	cookieSameSite http.SameSite
}

// SetCookieWithOption 设置响应头中的Set-Cookie字段 并将Context中关于
// cookie的配置应用到给定的cookie上
func (c *Context) SetCookieWithOption(cookie *http.Cookie) {
	cookie.SameSite = c.cookieSameSite
	http.SetCookie(c.Resp, cookie)
}
```

**不推荐这种做法.因为关于cookie的选项属性不需要挂在Context上.使用者创建cookie自行设置,设置好后调用`Context.SetCookie()`方法即可**.

## PART3. 支持错误页面

本节工程结构如下:

```
(base) yanglei@bogon 09-errPage % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
├── serverInterface.go
└── stringValue.go

0 directories, 13 files
```

我们通常有一个需求:如果一个响应返回了404,那么应该重定向到一个默认页面,比如说重定向到首页.该怎么处理这个需求?

这个需求棘手的点在于:**不是所有的404响应都是要重定向的**.比如异步加载数据的RESTful请求.假设需求为在打开页面之后异步请求用户详情接口,那么这个场景下,即使响应404了,也不应该再重定向了.

我们现在的实现如下:

```go
package setCookie

import (
	"net"
	"net/http"
)

// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = &HTTPServer{}

// HTTPServer HTTP服务器
type HTTPServer struct {
	router
}

// 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)
}
```

我们现在只是返回了一个"Not Found"的字符串,通常而言应该是重定向到一个用于"兜底"的错误页面.

WEB框架确实应该支持这种重定向到错误页面的功能,但这个功能不应该在Context这个抽象层级上支持.因为即使在Context这个抽象层级上支持了这个功能,使用者用起来也非常麻烦,因为使用者每次需要重定向到错误页面时,都需要自己调用

实现:

```go
package errPage

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
	// QueryValues 查询参数名值对
	queryValues url.Values
	// cookieSameSite cookie的SameSite属性 即同源策略
	cookieSameSite http.SameSite
}

// ErrPage 指定重定向到错误页面
func (c *Context) ErrPage() {
	// Tips: 此处用"this is a error page"模拟一个错误页面
	c.Resp.Write([]byte("this is a error page"))
}
```

很明显使用者需要自己调用这个`Context.ErrPage()`方法,才能返回错误页面.

那么如果在框架内部检测状态码,并确认是否需要重定向到错误页面,是否可行?

这里我们以`Context.RespJSON()`方法举例:

```go
package errPage

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
	// QueryValues 查询参数名值对
	queryValues url.Values
	// cookieSameSite cookie的SameSite属性 即同源策略
	cookieSameSite http.SameSite
}

// ErrPage 指定重定向到错误页面
func (c *Context) ErrPage() {
	// Tips: 此处用"this is a error page"模拟一个错误页面
	c.Resp.Write([]byte("this is a error page"))
}

// RespJSON 以JSON格式输出相应
func (c *Context) RespJSON(status int, obj any) (err error) {
	// 如果响应状态码为404 则重定向到错误页面
	if status == http.StatusNotFound {
		c.ErrPage()
		return nil
	}

	data, err := json.Marshal(obj)
	if err != nil {
		return err
	}

	c.Resp.Header().Set("Content-Type", "application/json")
	c.Resp.Header().Set("Content-Length", strconv.Itoa(len(data)))
	// Tips: 在写入响应状态码之前设置响应头 因为一旦调用了WriteHeader方法
	// Tips: 随后对响应头的任何修改都不会生效 因为响应头已经发送给客户端了
	c.Resp.WriteHeader(status)

	n, err := c.Resp.Write(data)
	if n != len(data) {
		return errors.New("web绑定错误: 写入响应体不完整")
	}

	return err
}
```

这里我们检测到响应码为404时,自动重定向到错误页面,也不可行.因为**不是所有的404响应都是要重定向的**.

在后续的AOP中,我们再来设计这个需求的解决方案


# 5.10 Context-总结与面试要点

本节课工程结构如下:

```
(base) yanglei@yuanhong 10-summary % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
├── serverInterface.go
└── stringValue.go

0 directories, 13 files
```

## PART1. Context是线程安全的吗？

显然不是.但是Context不需要被设计成线程安全的理由,和路由树不需要被设计成线程安全的理由不太一样.

路由树不需要被设计成线程安全,是因为按照我们的设计,当完成路由注册这个过程之后,WEB服务器才会被启动.相当于以WEB服务器启动这一事件为标记,在这个时刻之前,路由树被单线程写入;在这个时刻之后,路由树被多个goroutine读取.这意味着对于路由树而言,并没有并发读写的场景,因此根本不需要被设计成线程安全.

Context不需要保证线程安全,是因为按照我们的预期,这个Context只会被使用者在1个HandleFunc中使用,**且不应该被多个goroutine操作**(因为通常而言你并不会遇到很多个goroutine同时向`http.ResponseWriter`中写入的场景).

对于绝大多数人来说,他们不需要一个线程安全的Context.退一万步讲,如果真的需要一个线程安全的Context,那么**提供一个装饰器**,让用户使用前手动创建一个装饰器即可:

`safeContext.go`:

```go
package summary

import (
	"encoding/json"
	"errors"
	"net/http"
	"strconv"
	"sync"
)

// SafeContext 使用装饰器模式 为 Context 添加了一个互斥锁
// 实现了 Context 的线程安全
type SafeContext struct {
	Context Context
	Lock    sync.Mutex
}

// SetCookie 设置响应头中的Set-Cookie字段
func (s *SafeContext) SetCookie(cookie *http.Cookie) {
	s.Lock.Lock()
	defer s.Lock.Unlock()

	http.SetCookie(s.Context.Resp, cookie)
}

// BindJSON 绑定请求体中的JSON到给定的实例(这里的实例不一定是结构体实例,还有可能是个map)上
func (s *SafeContext) BindJSON(target any) error {
	s.Lock.Lock()
	defer s.Lock.Unlock()

	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if s.Context.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	decoder := json.NewDecoder(s.Context.Req.Body)
	return decoder.Decode(target)
}

// FormValue 获取表单中给定键的值
func (s *SafeContext) FormValue(key string) (stringValue StringValue) {
	s.Lock.Lock()
	defer s.Lock.Unlock()

	err := s.Context.Req.ParseForm()
	if err != nil {
		return StringValue{err: err}
	}

	return StringValue{value: s.Context.Req.FormValue(key)}
}

// QueryValue 获取查询字符串中给定键的值
func (s *SafeContext) QueryValue(key string) (stringValue StringValue) {
	s.Lock.Lock()
	defer s.Lock.Unlock()

	if s.Context.queryValues == nil {
		s.Context.queryValues = s.Context.Req.URL.Query()
	}

	if len(s.Context.queryValues) == 0 {
		return StringValue{err: errors.New("web绑定错误: 无任何查询参数")}
	}

	values, ok := s.Context.queryValues[key]
	if !ok {
		return StringValue{err: errors.New("web绑定错误: 查询参数中不存在键: " + key)}
	}

	return StringValue{value: values[0]}
}

// PathValue 获取路径参数中给定键的值
func (s *SafeContext) PathValue(key string) (stringValue StringValue) {
	s.Lock.Lock()
	defer s.Lock.Unlock()

	if s.Context.PathParams == nil {
		return StringValue{err: errors.New("web绑定错误: 无任何路径参数")}
	}

	value, ok := s.Context.PathParams[key]
	if !ok {
		return StringValue{err: errors.New("web绑定错误: 路径参数中不存在键: " + key)}
	}

	return StringValue{value: value}
}

// RespJSON 以JSON格式输出相应
func (s *SafeContext) RespJSON(status int, obj any) (err error) {
	s.Lock.Lock()
	defer s.Lock.Unlock()

	data, err := json.Marshal(obj)
	if err != nil {
		return err
	}

	s.Context.Resp.Header().Set("Content-Type", "application/json")
	s.Context.Resp.Header().Set("Content-Length", strconv.Itoa(len(data)))
	// Tips: 在写入响应状态码之前设置响应头 因为一旦调用了WriteHeader方法
	// Tips: 随后对响应头的任何修改都不会生效 因为响应头已经发送给客户端了
	s.Context.Resp.WriteHeader(status)

	n, err := s.Context.Resp.Write(data)
	if n != len(data) {
		return errors.New("web绑定错误: 写入响应体不完整")
	}

	return err
}

// RespJSONOK 以JSON格式输出一个状态码为200的响应
func (s *SafeContext) RespJSONOK(obj any) (err error) {
	s.Lock.Lock()
	defer s.Lock.Unlock()
	
	return s.Context.RespJSON(http.StatusOK, obj)
}
```

使用时手动创建即可:

`context_test.go`:

```go
package summary

import (
	"sync"
	"testing"
)

func TestContext_SafeContext(t *testing.T) {
	s := &HTTPServer{router: newRouter()}

	handleFunc := func(ctx *Context) {
		safeContext := &SafeContext{
			Context: *ctx,
			Lock:    sync.Mutex{},
		}

		type User struct {
			Name string `json:"name"`
		}

		// 获取路径参数
		id := safeContext.PathValue("name")

		user := &User{Name: id.value}

		safeContext.RespJSON(202, user)
	}

	s.GET("/user/:name", handleFunc)
	_ = s.Start(":8091")
}
```

当然,站在框架设计者的角度来看,是不需要提供一个线程安全的Context的.

但是,这种装饰器的思路是很有用的.比如你新接触了一个框架,你预期他会给你一个线程安全的结构体,但是他没有做到线程安全.那么也可以用这种思路去封装他提供给你的、非线程安全的结构体.

## PART2. Context为什么不设计为接口?

目前来看,看不出来设计为接口的必要性.

**Echo框架将Context设计为接口,但是只有一个实现**,就足以说明设计为接口有点过度设计的感觉.

即便Iris将Context设计为接口,且允许用户提供自定义实现,但是**看起来也不是那么有用**(因为没人提供自定义实现).

讲到这里,多提一嘴,之所以我们在设计HTTPServer时,设计了Server接口,是为了方便[设计HTTPSServer](https://github.com/rayallen20/GoInAction/blob/master/note/%E7%AC%AC1%E5%91%A8-Web%E6%A1%86%E6%9E%B6%E4%B9%8B%20Server%E4%B8%8E%E8%B7%AF%E7%94%B1%E6%A0%91%20/PART2.%20Server/6.%20Server%E8%AF%A6%E8%A7%A3%E4%B8%8E%E9%9D%A2%E8%AF%95%E8%A6%81%E7%82%B9.md#432-%E6%94%AF%E6%8C%81https)

## PART3. Context能不能用泛型?

我们已经在好几个地方用过泛型了(其实我在之前并没有用过,临场现补的.没用过的可以参考[我整理的泛型初步笔记](https://github.com/rayallen20/GoInAction/blob/master/note/%E7%AC%AC0%E5%91%A8-%E9%99%84%E5%BD%95/PART1.%20%E6%B3%9B%E5%9E%8B/1.%20%E6%B3%9B%E5%9E%8B.md)).在Context中,似乎也有使用泛型的场景.例如处理表单数据、查询参数、路径参数

一个比较常见的想法是:将`Context.QueryValue()`这种处理各部分输入的方法设计为泛型,这样直接可以返回用户所需的类型,如下:

```go
package summary

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
	"strconv"
)

// Context HandleFunc的上下文
type Context struct {
	// Req 请求
	Req *http.Request
	// Resp 响应
	Resp http.ResponseWriter
	// PathParams 路径参数名值对
	PathParams map[string]string
	// QueryValues 查询参数名值对
	queryValues url.Values
	// cookieSameSite cookie的SameSite属性 即同源策略
	cookieSameSite http.SameSite
}

// QueryValueGenericity 获取查询字符串中给定键的值 以泛型的方式返回
// 调用时直接指定泛型的类型 例: Context.QueryValueGenericity[int]("age")
func (c *Context) QueryValueGenericity[T any](key string) (T, error) {

}
```

理想很丰满现实很骨感,直接编译错误:

![编译错误](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-428c4f87d39399f3462a7fe8396327480fd8d1d8%2F%E7%BC%96%E8%AF%91%E9%94%99%E8%AF%AF.png?alt=media)

**因为结构体方法不允许使用类型参数**.

那么又有点子王想到,将StringValue做成泛型,实现如下:

```go
package summary

import "strconv"

// StringValue 用于承载来自各部分输入的值 并提供统一的类型转换API
type StringValue[T any] struct {
	// value 承载来自各部分输入的值 以字符串表示
	value string
	// err 用于承载处理各部分输入时的错误信息
	err error
}

// AsInt64 将承载的值转换为int64类型表示
func (s StringValue[T]) AsInt64() (t T, err error) {
	if s.err != nil {
		return any(0), s.err
	}

	value, err := strconv.ParseInt(s.value, 10, 64)
	if err != nil {
		return any(0), err
	}
	
	return any(value), nil
}

// AsUint64 将承载的值转换为uint64类型表示
func (s StringValue[T]) AsUint64() (t T, err error) {
	if s.err != nil {
		return any(0), s.err
	}

	value, err := strconv.ParseUint(s.value, 10, 64)
	if err != nil {
		return any(0), err
	}

	return any(value), nil
}

// AsFloat64 将承载的值转换为float64类型表示
func (s StringValue[T]) AsFloat64() (t T, err error) {
	if s.err != nil {
		return any(0), s.err
	}

	value, err := strconv.ParseFloat(s.value, 64)
	if err != nil {
		return any(0), err
	}

	return any(value), nil
}
```

那么我们考虑一下,在Context中创建StringValue时,该如何指定这个T的类型呢?将T指定为什么类型才是正确的呢?

![创建StringValue时指定类型](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-c124145ebb575f7a0d753412366d770a9c02ccbe%2F%E5%88%9B%E5%BB%BAStringValue%E6%97%B6%E6%8C%87%E5%AE%9A%E7%B1%BB%E5%9E%8B.png?alt=media)

答案是根本不知道.所以将StringValue做成泛型的方案也GG了.

## PART4. 面试要点

### 4.1 能否重复读取HTTP协议的Body内容(即`http.Request.Body`能否被重复读取)?

原生API是不可以的.但是我们可以通过封装来允许重复读取.核心步骤是我们将`http.Request.Body`读取出来之后放到一个地方,后续都从这个地方读取即可.

### 4.2 能否修改HTTP协议的响应?

原生API也是不可以的.但是可以用我们的RespData这种机制,在最后再把数据刷新到网络中,在刷新之前,都可以修改

这里所谓的原生API,指的是`http.ResponseWriter.Write()`方法.很明显这个方法写完了就将响应体刷到前端去了,写完之后改不了.后边引入RespData(现在还没讲了)机制,就可以实现在刷到前端之前都是可以修改的.

### 4.3 Form 和 PostForm 的区别?

[`http.Request.Form`与`http.Request.PostForm`的区别](https://github.com/rayallen20/GoInAction/blob/master/note/%E7%AC%AC2%E5%91%A8-Web%E6%A1%86%E6%9E%B6%E4%B9%8BContext%E4%B8%8EAOP%E6%96%B9%E6%A1%88/PART1.%20Context/7.%20Context-%E5%A4%84%E7%90%86%E8%BE%93%E5%85%A5%E4%B9%8B%E8%A1%A8%E5%8D%95%E8%BE%93%E5%85%A5.md#part1-httprequestform%E4%B8%8Ehttprequestpostform%E7%9A%84%E5%8C%BA%E5%88%AB)

正常的情况下你的API优先使用`http.Request.Form`就不太可能出错

### 4.4 Web框架是怎么支持路径参数的?

Web框架在发现匹配上了某个路径参数之后,将这段路径记录下来作为路径参数的值.这个值默认是string类型,用户自己可以转化为不同的类型

[查找路由时写入路径参数](https://github.com/rayallen20/GoInAction/blob/master/code/week1/server/v4/router.go#L143)

### 4.5 v5版本的实现

最终v5版本的实现如下(GoInAction/code/week2/context/v5):

```
(base) yanglei@bogon v5 % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

0 directories, 14 files
```


# PART06.AOP


# 6.01 AOP简介与不同框架设计概览

![学习路线](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e450d893fafdffbe075f08dc4e1606481b2e43fb%2F%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF.png?alt=media)

## PART1. 什么是AOP?

AOP:Aspect Oriented Programming,面向切面编程.核心在于将横向关注点从业务中剥离出来.

横向关注点:**与业务基本无关,但每个业务(或者说每个HandleFunc)又必须要处理的**.常见的有几类:

* **可观测性**:logging、metric和tracing
* **安全相关**:登录、鉴权与权限控制
* **错误处理**:例如错误页面支持
* **可用性保证**:熔断、限流和降级等

基本上WEB框架都会设计自己的AOP方案

## PART2. Beego的AOP方案设计

Beego早期的时候设计了完善的回调机制,大体上有3类:

### 2.1 MiddleWare

Beego中的[`web.MiddleWare`](https://github.com/beego/beego/blob/develop/server/web/server.go#L77),其类型为`func(http.Handler) http.Handler`,入参和返回值均为**GO原生的**[`http.Handler`](https://github.com/golang/go/blob/master/src/net/http/server.go#L86).注意是GO原生的`http.Handler`,不是Beego框架的`web.HTTPServer`

由于它的入参和返回值均为**GO原生的**`http.Handler`接口,而非Beego框架的[`web.HTTPServer`](https://github.com/beego/beego/blob/develop/server/web/server.go#L50).这导致MiddleWare基本脱离了Beego的控制.这意味着在MiddleWare中无法对Beego框架的`web.HTTPServer`进行二次封装或修改其中的字段值.换言之,使用MiddleWare就意味着使用者**基本脱离了Beego的控制**,也就是说这种场景下使用者绕过了Beego,直接和GO原生的`net/http`包交互了.

Beego的[`web.HTTPServer`](https://github.com/beego/beego/blob/develop/server/web/server.go#L50)本身就是一个`http.Handler`接口的实现.而使用MiddleWare就意味着用户需要自己再实现一遍GO原生的`http.Handler`接口,这个过程相当于用户自己定义了一个Beego的`web.HTTPServer`,然后通过MiddleWare将用户自己实现的`web.HTTPServer`与Beego的`web.HTTPServer`结合在一起.

注:因为Beego的[`web.HTTPServer`](https://github.com/beego/beego/blob/develop/server/web/server.go#L50)组合了Beego的[`web.ControllerRegister`](https://github.com/beego/beego/blob/develop/server/web/router.go#L160),而`web.ControllerRegister`又实现了GO原生的[`http.Handler`](https://github.com/golang/go/blob/master/src/net/http/server.go#L86)接口.因此说Beego的[`web.HTTPServer`](https://github.com/beego/beego/blob/develop/server/web/server.go#L50)本身就是一个Handler.

正常来讲,Beggo的HTTPServer都是最后一个,作为next传入到`web.MiddleWare`的入参中.然后`web.MiddleWare`再返回一个自己实现的`http.Handler`.这里听不懂没关系,我也没听懂,后边实现了就知道了.

### 2.2 Filter

[`web.FilterFunc`](https://github.com/beego/beego/blob/develop/server/web/filter.go#L31)允许用户注册**不同阶段运行的Filter**.这些Filter都是单向的,不是环绕式的.

实际上这个类型就是[`web.HandleFunc`](https://github.com/beego/beego/blob/master/server/web/router.go#L638)的别名.

所谓[不同阶段](https://github.com/beego/beego/blob/master/server/web/router.go#L38),指的是:

```go

// default filter execution points
const (
	// 处理静态资源之前执行的filter
	BeforeStatic = iota
	// 执行路由匹配之前执行的filter
	BeforeRouter
	// 执行业务逻辑(HandleFunc)之前执行的filter
	BeforeExec
	// 执行业务逻辑(HandleFunc)之后执行的filter
	AfterExec
	// 路由结束之后执行的filter
	FinishRouter
)
```

所谓单向的,是指你可以注册多个Filter,但是这些Filter只会在某个具体的时间点(或者可以说是具体的阶段)上执行.而这个具体的阶段一旦执行完成,就不会再往回了.你可以认为`BeforeStatic->BeforeRouter->BeforeExec->AfterExec->FinishRouter`是一路走下来的,不会回头的.因此说Filter是单向的

### 2.3 FilterChain

[`web.FilterChain`](https://github.com/beego/beego/blob/master/server/web/filter.go#L26)可以看做是一种可利用Beego内部数据的MiddleWare.**它将`Filter`组织成链**,每一个`Filter`都要考虑调用下一个`Filter`,否则就会终端执行.同时,这些`Filter`也失去了指定阶段运行的能力.

```go
// FilterChain is different from pure FilterFunc
// when you use this, you must invoke next(ctx) inside the FilterFunc which is returned
// And all those FilterChain will be invoked before other FilterFunc
type FilterChain func(next FilterFunc) FilterFunc
```

可以看到,其入参和返回值类型均为[`web.FilterFunc`](https://github.com/beego/beego/blob/develop/server/web/filter.go#L31)

### 2.4 Beego中AOP方案的案例

#### 2.4.1 APISecretAuth

[`apiauth.APISecretAuth()`](https://github.com/beego/beego/blob/master/server/web/filter/apiauth/apiauth.go#L87)函数用于检查能否将一个AppID转化为一个AppSecret,完成鉴权的过程

#### 2.4.2 FilterChain

[`opentracing.FilterChain()`](https://github.com/beego/beego/blob/master/server/web/filter/opentracing/filter.go#L34)用于在链路追踪的实现过程中,初始化span并为span设定一些值

AOP是最为简单,最为方便扩展的.所以作为一个中间件设计者,**并不是提供越多实现越好**,更好的做法是只提供绝大多数用户会用得到,且各个不同实现用起来都没什么区别的实现.

## PART3. GIN的AOP方案设计

### 3.1 HandlersChain

GIN的设计稍微不同.在Beego中,`FilterChain`是通过主动调用`web.FilterFunc()`的方式,实现从一个`web.FilterFunc()`执行到另一个`web.FilterFunc()`的.相当于每一个`web.FilterFunc()`自由决定要不要调用下一个`web.FilterFunc()`.

GIN采用的是**半集中式设计**,由`Context`负责调度.但实际上也是`HandlerFunc`在自己的函数体内主动调用下一个`HandlerFunc`

[`gin.Context.handlers`](https://github.com/gin-gonic/gin/blob/master/context.go#L57)的类型为[`gin.HandlersChain`](https://github.com/gin-gonic/gin/blob/master/gin.go#L51)

注意,`gin.HandlersChain`的实现如下:

```go
type HandlersChain []HandlerFunc
```

可以看到`gin.HandlersChain`是一个slice;而Beego中的`web.FilterChain`是一个单一的函数.这是两者实现之间的区别.

在GIN中,`gin.HandlersChain`被定义在了`Context`抽象层级上,而`Context`中定义了[`gin.Context.Next()`](https://github.com/gin-gonic/gin/blob/master/context.go#L171)(控制执行下一个HandlerFunc)方法和[`gin.Context.Abort()`](https://github.com/gin-gonic/gin/blob/master/context.go#L188)(控制终端后续的HandlerFunc)等方法.这些方法用于控制`Context.handlers`中,HandlerFunc是否执行.

之所以说GIN的AOP方案是半集中式的,是因为虽然是`HandlersChain`中的每个`HandlerFunc`在自己的函数体内主动调用下一个`HandlerFunc`,但是是否执行下一个`HandlerFunc`,需要靠`Context.Next()`和`Context.Abort()`(本质上是`Context.index`)来控制的.

完全的集中式是由一个中央管理器之类的统一调度各种Filter或者Handler是否执行,在中间件设计里面不常用.因为每个路由可能都有其自己要执行或不执行的Handler.

### 3.2 GIN中AOP方案的案例

#### 3.2.1 metric实现

在[`ginmetrics.Monitor.monitorInterceptor()`](https://github.com/penglongli/gin-metrics/blob/b66ef4a3274e50cfc651a5639ee4a66bcbf5d0b8/ginmetrics/middleware.go#L103)方法的实现中可以看到这一行:`ctx.Next()`.这也就是上文所说的**每个`HandlerFunc`在自己的函数体内主动调用下一个`HandlerFunc`**.

#### 3.2.2 tracing实现

在[`ginhttp.Middleware()`](https://github.com/opentracing-contrib/go-gin/blob/1499e56cc90644a5cb0f94ead2904ba284e46f6a/ginhttp/server.go#L67)函数中也能看到`c.Next()`这种调用方式.

## PART4. Echo的AOP方案设计

### 4.1 MiddlewareFunc

Echo的[`echo.MiddlewareFunc`](https://github.com/labstack/echo/blob/master/echo.go#L124)其实现如下:

```go
// MiddlewareFunc defines a function to process middleware.
MiddlewareFunc func(next HandlerFunc) HandlerFunc
```

可以看到和Beego的FilterChain基本相同,也是依赖于`echo.MiddlewareFunc`返回的`echo.HandlerFunc`主动调用下一个`echo.HandlerFunc`.

### 4.2 Echo中AOP方案的案例

#### 4.2.1 CSRF防护

[`middleware.CSRFWithConfig()`](https://github.com/labstack/echo/blob/584cb85a6b749846ac26a8cd151244ab281f2abc/middleware/csrf.go#L97)函数中,也可以看到`next(c)`这种代码,思路上也是**每个`HandlerFunc`在自己的函数体内主动调用下一个`HandlerFunc`**

## PART5. Iris的AOP方案设计

### 5.1 router.WrapperFunc

Iris的AOP设计方案与Beego的方案本质上没有区别,只是换了个名字,以及构造方式上有差别.

Iris中的[`router.WrapperFunc`](https://github.com/kataras/iris/blob/main/core/router/router_wrapper.go#L9)本质上和Beego的Filter是相同的.

[`router.makeWrapperFunc()`](https://github.com/kataras/iris/blob/583bd3eb6b4e9342c6ab897a1f3047fbf89a0207/core/router/router_wrapper.go#L11)函数负责构造WrapperFunc链.构造方式上来讲,核心思路也是**每个`WrapperFunc`在自己的函数体内主动调用下一个`WrapperFunc`**:

```go
func makeWrapperFunc(original WrapperFunc, wrapperFunc WrapperFunc) WrapperFunc {
	if wrapperFunc == nil {
		return original
	}

	if original != nil {
		// wrap into one function, from bottom to top, end to begin.
		nextWrapper := wrapperFunc
		prevWrapper := original
		wrapperFunc = func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
			// 下一个WrapperFunc不为空则手动调用下一个
			if next != nil {
				nexthttpFunc := http.HandlerFunc(func(_w http.ResponseWriter, _r *http.Request) {
					prevWrapper(_w, _r, next)
				})
				nextWrapper(w, r, nexthttpFunc)
			}
		}
	}

	return wrapperFunc
}
```

## PART6. 总结

可以看到,除了GIN之外,Beego、Echo、Iris都是一样的.

有一种观点认为,GIN的那种将责任链放在Context上的设计是不如Beego、Echo、Iris的.猜测GIN的设计是为了在请求粒度上控制责任链中的每个Handler是否执行.但是正常情况下可能并不太需要在请求粒度上控制,通常而言在路由粒度上控制责任链中的每个Handler是否执行即可.

换言之,通常而言,2个请求命中了同一个路由,那么对于这个路由而言,它要执行的Handler就是相同的.而GIN可以做到的是:2个请求命中同一个路由,但这2个请求分别要执行的Handler是不同的.

个人经验上来讲,这个控制粒度精确到路由级别已经是足够用了.

我们后续实现的控制粒度在Server上,而作业需要改写为控制粒度在路由级别.


# 6.02 AOP设计方案-Middleware

本节课工程结构如下:

```
(base) yanglei@bogon 01-middleware % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── node.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

0 directories, 14 files
```

## PART1. 定义Middleware

### 1.1 非集中式的设计方案

非集中式的设计方案,就是将调用下一个Middleware的能力交由每一个Middleware,由Middleware本身根据一些逻辑决定是否执行下一个Middleware

#### 1.1.1 函数式的洋葱模式方案

这里我们也将我们的中间件类型命名为Middleware.它的定义也是入参和返回值的类型均为`HandleFunc`:

`middleware.go`:

```go
package middleware

// Middleware 中间件 用来包装 HandleFunc 返回一个新的 HandleFunc
// 这种入参和返回值均为一个函数的设计 是函数式编程 通过这种方式 可以将多个中间件串联起来
// 函数式的洋葱模式 或者叫 函数式的责任链模式
type Middleware func(next HandleFunc) HandleFunc
```

在GO中广泛采用这种方案

#### 1.1.2 非函数式的设计方案

`middleware.go`:

```go
package middleware

// Middleware 中间件接口
type Middleware interface {
	// Invoke 包装 HandleFunc 返回一个新的 HandleFunc
	Invoke(next HandleFunc) HandleFunc
}
```

或者使用拦截器模式:

`middleware.go`:

```go
package middleware

// Interceptor 拦截器接口
type Interceptor interface {
	// Before 前置拦截器(在请求处理之前执行)
	Before(ctx *Context)
	// After 后置拦截器(在请求处理之后执行)
	After(ctx *Context)
	// Surround 环绕拦截器(在请求处理前后执行)
	Surround(ctx *Context)
}
```

在JAVA中广泛采用这种方案

### 1.2 集中式的设计方案

这种方案类似GIN的设计:

`middleware.go`:

```go
package middleware

// HandleChan 中间件链
type HandleChan []HandleFunc
```

或者可以将其设计为一个结构体,但本质上集中式的设计方案会导致中间件链的执行比较死板,无法决定中间件链中的某个(某几个)中间件针对指定的路由是否执行:

`middleware.go`:

```go
package middleware

// HandlerChan 中间件链
type HandlerChan struct {
	// handlers 用于保存中间件链的切片
	handlers []HandleFunc
}

// Run 顺序执行中间件链上的每一个中间件
func (h HandlerChan) Run(ctx *Context) {
	for _, handler := range h.handlers {
		handler(ctx)
	}
}
```

或者也可以做一些比较简单的控制:

`middleware.go`:

```go
package middleware

// HandleFuncNext 用于演示可控制是否执行下一个中间件的中间件函数
type HandleFuncNext func(ctx *Context) (next bool)

// HandlerChan 中间件链
type HandlerChan struct {
	// handlers 用于保存中间件链的切片
	handlers []HandleFuncNext
}

// Run 顺序执行中间件链上的每一个中间件 直到某个中间件指定不再执行下一个中间件
func (h HandlerChan) Run(ctx *Context) {
	for _, handler := range h.handlers {
		next := handler(ctx)
		if !next {
			return
		}
	}
}
```

在JAVA中广泛采用这种方案

这里多提一嘴,AOP方案在不同的框架,不同的语言中有不同的叫法,比如:Middleware/Handler/Chain/Filter/FilterChain.Interceptor/Wrapper

## PART2. 调用Middleware

这里我们在Server级别上调用Middleware,作业是需要实现router级别的Middleware

`httpServer.go`:

```go
// HTTPServer HTTP服务器
type HTTPServer struct {
	router

	// 中间件切片 表示HTTPServer需要按顺序执行的的中间件链
	middlewares []Middleware
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 执行中间件链
	root := s.serve
	for i := len(s.middlewares) - 1; i >= 0; i-- {
		root = s.middlewares[i](root)
	}

	// 查找路由树并执行命中的业务逻辑
	root(ctx)
}
```

注:这里我第一次写时,是有疑问的:为什么`root`(也就是`s.serve`)可以作为`Middleware()`的入参?这个疑问见附录

## PART3. 测试

这里先不解释为什么从后往前遍历,先写一个测试用例,结合测试用例来讲解这个实现:

`middleware_test.go`:

```go
package middleware

import (
	"fmt"
	"net/http"
	"testing"
)

// Test_Middleware 测试中间件的工作顺序
func Test_Middleware(t *testing.T) {
	s := NewHTTPServer()

	s.middlewares = []Middleware{
		Middleware1,
		Middleware2,
		Middleware3,
		Middleware4,
	}

	s.ServeHTTP(nil, &http.Request{})
}

func Middleware1(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件1开始执行")
		next(ctx)
		fmt.Println("中间件1结束执行")
	}
}

func Middleware2(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件2开始执行")
		next(ctx)
		fmt.Println("中间件2结束执行")
	}
}

func Middleware3(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件3中断后续中间件的执行")
	}
}

func Middleware4(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件4不会被执行")
	}
}
```

执行结果如下:

```
GOROOT=/usr/local/go #gosetup
GOPATH=/Users/yanglei/Desktop/myGoPath #gosetup
/usr/local/go/bin/go test -c -o /Users/yanglei/Library/Caches/JetBrains/GoLand2023.2/tmp/GoLand/___Test_Middleware_in_middleware.test middleware #gosetup
/usr/local/go/bin/go tool test2json -t /Users/yanglei/Library/Caches/JetBrains/GoLand2023.2/tmp/GoLand/___Test_Middleware_in_middleware.test -test.v -test.paniconexit0 -test.run ^\QTest_Middleware\E$
=== RUN   Test_Middleware
中间件1开始执行
中间件2开始执行
中间件3中断后续中间件的执行
中间件2结束执行
中间件1结束执行
--- PASS: Test_Middleware (0.00s)
PASS

Process finished with the exit code 0
```

赋值与执行流程如下图示:

![中间件赋值与执行流程](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e3a7e97d0c3db48204ca570f511a44aab2abb92e%2F%E4%B8%AD%E9%97%B4%E4%BB%B6%E8%B5%8B%E5%80%BC%E4%B8%8E%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B.jpg?alt=media)

从执行的流程中可以看出,实际上中间件的执行过程和递归非常类似.只是递归的场景下,压栈/出栈的是同一个函数;而中间件的执行过程中,每一次压栈/出栈的函数是不同的.

但本质上,都是靠栈顶的函数返回,来触发下一个函数的出栈(下一个函数需要等待栈顶函数返回才能执行后续代码进而完成出栈),反复此过程直到所有的函数都出栈.

正常执行时序图如下:

![正常执行时序图](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-31a2eba9f15ff7fccecf662bde6a88d57927139f%2F%E6%AD%A3%E5%B8%B8%E6%89%A7%E8%A1%8C%E6%97%B6%E5%BA%8F%E5%9B%BE.png?alt=media)

某个中间件中没有调用`next()`,那么执行流程就被打断了,时序图如下:

![被打断的时序图](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-a2849357db4bf7e504ca7928ae7c19764d8e9fc6%2F%E8%A2%AB%E6%89%93%E6%96%AD%E7%9A%84%E6%97%B6%E5%BA%8F%E5%9B%BE.png?alt=media)

**所以实际上,从后往前遍历的过程,类似于压栈,先入后出**.

## PART4. 面试要点

### 4.1 什么是AOP?

AOP就是**面向切面编程**,用于解决横向关注点问题,如可观测性问题、安全问题等

在AOP中经常使用到洋葱模式和责任链模式

### 4.2 什么是洋葱模式?

形状如同洋葱,拥有一个核心,这个核心一般就是业务逻辑.而后在这个核心的外边层层包裹,每一层其实就是一个Middleware.一般用洋葱模式来**无侵入式**地增强核心功能,或者解决AOP问题

**无侵入式的设计方案很能体现一个人对问题的理解能力和对问题的抽象能力以及编程功底,甚至你可以认为:但凡是侵入的解决方案总是不好的,无侵入式的方案总是好的**.只有在逼不得已的情况下才采用侵入式设计方案,这里说的逼不得已通常是指为了提升性能.

因为无侵入式的设计方案,需要一层层的函数调用,相比于直接把这些中间件的代码写入到业务处理函数中,性能是要慢的,因为要发起方法调用

### 4.3 什么是责任链模式?

不同的HandleFunc组成一条链,链条上的每一环(每一个元素)都有自己的功能.一方面可以用责任链模式将复杂逻辑分成链条上的不同步骤,另一方面也可以灵活地在链条上添加新的HandleFunc

通常我们提到责任链时,指的是单向的责任链,最终调用到业务处理函数.但实际上本节课程中我们实现的"还能往回走"的(也就是执行测试用例时看到的`中间件2结束执行`和`中间件1结束执行`)方案,也是责任链模式

这里多提一嘴,责任链还有网状结构,大致实现如下:

```go
package middleware

import "sync"

// Net 责任链的网状结构
type Net struct {
	// handlers 用于保存责任链的切片 注意切片中的每一个元素都是一条责任链
	handlers []ConcurrentHandleFunc
}

// Run 执行责任网上的每一个责任链
func (n Net) Run(ctx *Context) {
	wg := sync.WaitGroup{}
	for _, handler := range n.handlers {
		h := handler
		if h.concurrent {
			wg.Add(1)
			go func() {
				h.Run(ctx)
				wg.Done()
			}()
		} else {
			h.Run(ctx)
		}
	}
	wg.Wait()
}

// ConcurrentHandleFunc 允许并发执行责任链上的每一个中间件
type ConcurrentHandleFunc struct {
	// concurrent 标识是否允许并发执行的标量
	concurrent bool
	// handlers 用于保存中间件链的切片
	handlers []*ConcurrentHandleFunc
}

// Run 执行责任链上的每一个中间件 通过并发标识决定是否允许并发执行
func (c ConcurrentHandleFunc) Run(ctx *Context) {
	for _, handler := range c.handlers {
		h := handler
		if h.concurrent {
			go h.Run(ctx)
		} else {
			h.Run(ctx)
		}
	}
}
```

### 4.4 如何实现?

最简单的方案就是我们课程上讲的这种函数式方案,还有一种是集中调度的模式

最终v6版本的实现如下(GoInAction/code/week2/aop/v6):

```
(base) yanglei@yuanhong v6 % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── node.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

0 directories, 16 files
```

## 附录

### 1. 方法值

在Go语言中,方法值是一种特殊的语法,它允许你**引用一个特定对象的方法作为一个值**.当你这么做时,你得到的是一个**绑定了特定接收者的函数**(注意你得到的是一个**函数**).换言之,方法值是一个已经将方法的接收者(通常是一个结构体的实例)固定下来的**函数**.

例如,假设你有一个结构体`HTTPServer`和一个方法`serve`:

```go
type HTTPServer struct {
    // ...
}

func (s *HTTPServer) serve(ctx *Context) {
    // ...
}
```

当你创建一个方法值时,你实际上是将`s.serve`作为一个**函数**来引用,而这个函数会自动接收`s`作为它的接收者.

来看如下代码:

```go
var s *HTTPServer
serveFunc := s.serve
```

在上面的代码中,变量`serveFunc`现在就是一个函数,可以像使用其他函数一样使用它,不需要再指定接收者`s`:

```go
var ctx *Context
// ...
// 这实际上调用的是 s.serve(ctx)
serveFunc(ctx)
```

在这个调用中,`s`是隐含的,你不需要显式传递它作为参数.

方法值的这种特性使得你可以非常方便地**将方法传递给期望函数的地方,而不需要额外创建一个匿名函数或闭包来绑定方法的接收者**.这在处理事件处理器、回调或者中间件等概念时特别有用.

### 2. 为什么`root`可以作为`Middleware()`的入参?

在Go语言中,你可以在一个方法和一个具有相同签名的函数类型之间进行转换,但这需要满足一定的条件.而在代码`s.middlewares[i](root)`中,是没有类型转换问题的.因为变量`root`在这行代码中,是作为一个`HandleFunc`类型被传递的.而且变量`root`被赋值为`s.serve`,这是一个**方法值**.

在Go中,当你引用一个特定的方法时,你实际上是创建了一个绑定了接收者的**函数值**.

假设你有一个方法:

```go
func (s *HTTPServer) serve(ctx *Context) {
    // serve 方法的实现
}
```

然后这样引用它:

```go
root := s.serve
```

**这里实际上创建了一个类型为`func(ctx *Context)`的函数**.这个函数是`HTTPServer.serve`方法的一个绑定实例,绑定到了`s`这个具体的`HTTPServer`实例上.这就是为什么你可以将`s.serve`赋值给一个变量或者将其作为参数传递的原因:因为你传递的并不是方法本身,而是传递了一个**方法值(再次强调,方法值就是绑定了特定接收者的函数,本质上还是函数)**,而这个方法值绑定了一个特定的接收者.

所以在这行代码`s.middlewares[i](root)`中,`root`(`s.serve`)被隐式转换为一个函数,这个函数匹配`HandleFunc`类型.

TODO:这里如果要确定是否有隐式转换,需要将GO编译成汇编,再从汇编反编译成GO,才能确认.但是这个我不会弄,如果有大佬麻烦搞完之后提个pr的,感激不尽.


# PART07.Middleware


# 7.01 Middleware-AccessLog

![学习路线](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-4246261d1d96e64115f9ab34632a705eff783440%2F%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF.png?alt=media)

## PART1. 为什么要有AccessLog?

在日常工作中,我们可能希望**能够记录所有进来的请求,以支持Debug**.这也就是所谓的AccessLog.

Beego、Iris、Gin都支持了AccessLog.这里我们也提供一个简单的AccessLog Middleware

## PART2. 实现

初态目录结构如下:

```
(base) yanglei@yuanhong 01-accessLog % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── node.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

0 directories, 16 files
```

### 2.1 创建中间件的过程

创建目录:

* `/middlewares`: 用于存放所有中间件的代码
* `/middlewares/access_log`: 用于存储accessLog中间件的代码

```
(base) yanglei@yuanhong 01-accessLog % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│   └── access_log
├── node.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

2 directories, 16 files
```

这里使用生成器模式来完成中间件的创建.先将生成器模式的结构写出来,不考虑具体的日志记录功能:

`middlewares/access_log/middlewareBuilder.go`:

```go
package access_log

import "web"

// MiddlewareBuilder accessLog中间件的构建器
type MiddlewareBuilder struct{}

func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 记录日志
			next(ctx)
		}
	}
}
```

### 2.2 定义中间件结构

这里我们假定需要记录的日志内容如下:

* 请求的主机地址
* 命中的路由
* 请求的HTTP动词
* 请求的uri(路径)

`middlewares/access_log/accessLog.go`:

```go
package access_log

// accessLog 定义访问日志的结构
type accessLog struct {
	Host       string `json:"host,omitempty"`       // Host 主机地址
	Route      string `json:"route,omitempty"`      // Route 命中的路由
	HTTPMethod string `json:"HTTPMethod,omitempty"` // HTTPMethod 请求的HTTP方法
	Path       string `json:"path,omitempty"`       // Path 请求的路径 即请求的uri部分
}
```

### 2.3 获取要记录的字段值

#### 2.3.1 基本实现

这里我们开始实现日志记录,完善`MiddlewareBuilder.Build()`方法

`middlewares/access_log/middlewareBuilder.go`:

```go
package access_log

import "web"

// MiddlewareBuilder accessLog中间件的构建器
type MiddlewareBuilder struct{}

func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 调用业务处理函数完成后记录日志
			defer func() {
				accessLogObj := accessLog{
					Host:       ctx.Req.Host,
					// TODO: Route字段的值需要从命中的路由处获取
					Route:      "",
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}
				// TODO: 写入日志
			}()
			next(ctx)
		}
	}
}
```

这里选择在defer中记录请求的原因:

1. `next(ctx)`可能会出现panic,如果将记录请求的代码直接写在调用`next(ctx)`后边,那么有可能不会被执行
2. 当然你也可以设计为在调用`next(ctx)`前记录请求,但如果在记录请求的过程中出现了panic,那么就会因为一些非关键流程的错误导致整个流程跑不通,是一种得不偿失的做法
3. 综上所述,写在`next(ctx)`,或者写在`next(ctx)`后,都有各自的问题.还是写在defer中最合适

这里我们遇到了第一个问题:命中的路由从哪里来?

#### 2.3.2 记录命中的路由

首先毫无疑问的一点是:命中的路由也得从`web.Context`中拿.那很明显现在的`web.Context`没有这个字段,需要加上:

`context.go`:

```go
// Context HandleFunc的上下文
type Context struct {
	Req            *http.Request       // Req 请求
	Resp           http.ResponseWriter // Resp 响应
	PathParams     map[string]string   // PathParams 路径参数名值对
	queryValues    url.Values          // queryValues 查询参数名值对
	cookieSameSite http.SameSite       // cookieSameSite cookie的SameSite属性 即同源策略
	MatchRoute     string              // MatchRoute 命中的路由
}
```

第2个问题就是:在哪里给这个字段赋值?

很明显是在查找到命中的路由之后,即`HTTPServer.serve()`方法中赋值:

`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

	// 命中节点则将节点的路由设置到上下文中
	ctx.MatchRoute = targetNode.node.path

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

注:

* 很明显此处我们只是记录了命中的路由的最后一段,即:假设注册的路由为`/a/b/*`,此处我们只是记录了`*`.这个属于细节问题,后续再调整.我写代码时也应该是这样:**先实现整体的结构,再调整细节**

#### 2.3.3 获取命中的路由

到这一步就比较顺理成章了:

`middlewares/access_log/middlewareBuilder.go`:

```go
package access_log

import "web"

// MiddlewareBuilder accessLog中间件的构建器
type MiddlewareBuilder struct{}

func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 调用业务处理函数完成后记录日志
			defer func() {
				accessLogObj := accessLog{
					Host:       ctx.Req.Host,
					Route:      ctx.MatchRoute,
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}
				// TODO: 写入日志
			}()
			next(ctx)
		}
	}
}
```

### 2.4 记录请求

到了这一步,剩下的就是记录日志了.有的人(包括我)到这一步可能直接就打印了:

`middlewares/access_log/middlewareBuilder.go`:

```go
package access_log

import (
	"log"
	"web"
)

// MiddlewareBuilder accessLog中间件的构建器
type MiddlewareBuilder struct{}

func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 调用业务处理函数完成后记录日志
			defer func() {
				accessLogObj := accessLog{
					Host:       ctx.Req.Host,
					Route:      ctx.MatchRoute,
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}
				log.Print(accessLogObj)
			}()
			next(ctx)
		}
	}
}
```

那么问题来了:如果框架的使用者,不希望使用GO原生的log包来打印日志(甚至使用者的需求不是打印日志而是将日志写入文件等),该怎么办?

因此应该提供一种方式,让**框架的使用者从外部定义打印日志的过程**:

#### 2.4.1 对外暴露控制记录方式的接口

这里的"接口",指的不是编码时的`interface`,而是提供给外部的、让外部控制记录方式的一个"入口"(这里我确实没找到合适的词来形容)

`middlewares/access_log/middlewareBuilder.go`:

```go
package access_log

import (
	"log"
	"web"
)

// MiddlewareBuilder accessLog中间件的构建器
type MiddlewareBuilder struct {
	logFunc func(logContent string) // logFunc 记录日志的函数,该函数用于提供给外部,让使用者自定义日志记录的方式
}

// SetLogFunc 设置记录日志的函数并返回构建器.返回构建器的目的在于:在使用时可以链式调用
// 即:使用时可以写出如下代码:
// m := &MiddlewareBuilder{}
//
//	m.SetLogFunc(func(logContent string) {
//		log.Print(logContent)
//	}).Build()
func (m *MiddlewareBuilder) SetLogFunc(logFunc func(logContent string)) *MiddlewareBuilder {
	m.logFunc = logFunc
	return m
}
```

1. 先定义了一个函数类型的字段,该字段用于存储外部提供的记录日志函数
2. 再定义一个方法,该方法对外暴露,用于让外部设置记录日志函数

**这是典型的生成器模式的应用**.注意`MiddlewareBuilder.SetLogFunc()`方法的返回值,仍旧是一个`MiddlewareBuilder`的实例,这样的设计是为了在使用时可以链式调用,更加方便

#### 2.4.2 使用外部提供的函数记录日志

这样再记录日志就很容易了.

`middlewares/access_log/middlewareBuilder.go`:

```go
// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 调用业务处理函数完成后记录日志
			defer func() {
				accessLogObj := accessLog{
					Host:       ctx.Req.Host,
					Route:      ctx.MatchRoute,
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}

				accessLogBytes, _ := json.Marshal(accessLogObj)
				m.logFunc(string(accessLogBytes))
			}()
			next(ctx)
		}
	}
}
```

这里就是单纯的将`accessLog` 结构体的实例转为一个JSON,以便后续能够将其承载的数据转换为string类型.

## PART3. 传递中间件

先写测试用例,以便发现问题:

`middlewares/access_log/middleware_test.go`:

```go
package access_log

import (
	"fmt"
	"testing"
	"web"
)

// Test_MiddlewareBuilder 测试记录日志中间件
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建记录日志中间件
	builder := &MiddlewareBuilder{}
	accessLogMiddleware := builder.SetLogFunc(func(logContent string) {
		fmt.Println(logContent)
	}).Build()

	// 创建HTTPServer
	s := web.NewHTTPServer()
	// TODO: 怎么将中间件链添加到HTTPServer中?
}
```

现在问题出现了:该怎么将中间件加入到HTTPServer中?

### 3.1 方案1:实例化`HTTPServer`时传入中间件链

#### 3.1.1 实现

`httpServer.go`:

```go
// NewHTTPServerWithMiddleware 根据给定的中间件创建HTTP服务器
// 用这种方式创建中间件链的缺点在于: 缺乏扩展性
// 具体而言:
// 1. 如果以后的需求变更导致需要在middlewares前后添加其他的参数的话,整个函数的签名就都变了
// 2. 中间件链的顺序在创建`HTTPServer`实例时就固定了,无法灵活调整
func NewHTTPServerWithMiddleware(middlewares ...Middleware) *HTTPServer {
	return &HTTPServer{
		router:      newRouter(),
		middlewares: middlewares,
	}
}
```

#### 3.1.2 使用

`middlewares/access_log/middleware_test.go`:

```go
// Test_MiddlewareBuilder 测试记录日志中间件
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建记录日志中间件
	builder := &MiddlewareBuilder{}
	accessLogMiddleware := builder.SetLogFunc(func(logContent string) {
		fmt.Println(logContent)
	}).Build()

	// 创建HTTPServer
	s := web.NewHTTPServerWithMiddleware(accessLogMiddleware)
	s.Start(":8080")
}
```

#### 3.1.3 优缺点

优点:

1. 直观,好理解

缺点:

1. 如果以后的需求变更导致需要在形参`middlewares`前后添加其他的参数的话,整个函数的签名就都变了.可想而知受这个函数签名变化的影响,要修改的地方会有很多
2. 中间件链的顺序在创建`HTTPServer`实例时就固定了,无法灵活调整

### 3.2 方案2:将`HTTPServer.middlewares`字段设置为公有属性

#### 3.2.1 实现

`httpServer.go`:

```go
// HTTPServer HTTP服务器
type HTTPServer struct {
	router                   // router 路由树
	Middlewares []Middleware // Middlewares 中间件切片.表示HTTPServer需要按顺序执行的的中间件链
}
```

#### 3.2.2 使用

`middlewares/access_log/middleware_test.go`:

```go
// Test_MiddlewareBuilder 测试记录日志中间件
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建记录日志中间件
	builder := &MiddlewareBuilder{}
	accessLogMiddleware := builder.SetLogFunc(func(logContent string) {
		fmt.Println(logContent)
	}).Build()

	// 创建HTTPServer
	s := web.NewHTTPServer()
	if s.Middlewares == nil {
		s.Middlewares = make([]web.Middleware, 0)
	}
	s.Middlewares = append(s.Middlewares, accessLogMiddleware)
}
```

#### 3.2.3 优缺点

优点:

1. 直观,好理解

缺点:

1. 使用者调用了`NewHTTPServer()`函数之后,还要单独设置`HTTPServer.Middlewares`字段.似乎看起来也不方便使用
2. 不够灵活.换言之,**抽象层级不够高**

### 3.3 Option模式

其实我们要做的就是一件事:把在包(在我的示例代码中是`web`包)的外部创建的中间件,传入到`HTTPServer`结构体的实例上去.

我第一反应的想法:

```go
// SetMiddleware 本方法用于设置HTTPServer实例的middlewares属性
func (s *HTTPServer) SetMiddleware(middlewares ...Middleware)  {
	s.middlewares = middlewares
}
```

我们把这个思路的抽象层级再提升一层:**如果把"为`HTTPServer`结构体的属性赋值"这件事看做是一个"职责"(这里的职责是指设计模式原则中的"单一职责"),会怎么样?**

很明显能得到一个答案:需要一个类来完成这个职责.至此,Option模式的第一步出现了:创建`Option`类.

#### 3.3.1 创建HTTPServerOption结构体

`option.go`:

```go
package web

// Option HTTPServer的选项
// 不同的 Option 用于设置HTTPServer实例的不同属性
type Option func(server *HTTPServer)
```

注意这个结构体的类型为`func(server *web.HTTPServer)`,我们在这个函数的内部修改`web.HTTPServer`实例的属性值即可

第2个问题随之而来:怎么修改`web.HTTPServer`实例的属性值?

#### 3.3.2 实现修改`web.HTTPServer`实例的属性值的函数

这个问题比较容易回答:直接在函数体内部修改就行了,反正结构体实例的指针都已经拿到了

`httpServer.go`:

```go
// ServerWithMiddleware 本函数用于为HTTPServer实例添加中间件
// 即: 本函数用于设置HTTPServer实例的middlewares属性
func ServerWithMiddleware(middlewares ...Middleware) middlewares.Option {
	return func(server *HTTPServer) {
		server.middlewares = middlewares
	}
}
```

第3个问题又来了:什么时候修改属性值?

#### 3.3.3 调用函数修改`web.HTTPServer`实例的属性值

答案也比较简单:当然是创建出`HTTPServer`实例之后修改了

`httpServer.go`:

```go
// NewHTTPServer 创建HTTP服务器
// 这里选项的含义其实是指不同的 Option 函数
// 每一个 Option 函数都会对 HTTPServer 实例的不同属性进行设置
func NewHTTPServer(opts ...middlewares.Option) *HTTPServer {
	server := &HTTPServer{
		router: newRouter(),
	}

	for _, opt := range opts {
		opt(server)
	}

	return server
}
```

![使用Option模式创建HTTPServer实例](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-eb9d130030ae7d42bc6466ea04da477ec870da1a%2F%E4%BD%BF%E7%94%A8Option%E6%A8%A1%E5%BC%8F%E5%88%9B%E5%BB%BAHTTPServer%E5%AE%9E%E4%BE%8B.jpg?alt=media)

至此我们完成了使用Option模式创建HTTPServer实例的过程,继续回到测试代码上

## PART4. 测试并调试

### 4.1 以创建`http.Request`的方式编写测试用例

这里我们先手动创建一个请求,因为我们现在写的这个测试用例,是为了测试主逻辑是否正确的

`middlewares/access_log/middleware_test.go`:

```go
package access_log

import (
	"fmt"
	"net/http"
	"testing"
	"web"
)

// Test_MiddlewareBuilder 测试记录日志中间件
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建记录日志中间件
	builder := &MiddlewareBuilder{}
	// 注意这里的链式调用 使得在创建中间件的同时就可以设置记录日志的函数
	accessLogMiddleware := builder.SetLogFunc(func(logContent string) {
		fmt.Println(logContent)
	}).Build()

	// 创建中间件Option
	middlewareOption := web.ServerWithMiddleware(accessLogMiddleware)

	// 创建服务器
	// 这里就可以看出Option模式的好处了: 通过不同的Option函数可以设置HTTPServer实例的不同属性
	s := web.NewHTTPServer(middlewareOption)

	// 注册路由
	s.GET("/a/b/*", func(ctx *web.Context) {
		fmt.Println("hello, it's me")
	})

	// 创建请求
	request, err := http.NewRequest(http.MethodGet, "/a/b/c", nil)
	if err != nil {
		t.Fatal(err)
	}

	// 启动服务器
	s.ServeHTTP(nil, request)
}
```

运行结果:

```
=== RUN   Test_MiddlewareBuilder
hello, it's me
{"route":"*","HTTPMethod":"GET","path":"/a/b/c"}
--- PASS: Test_MiddlewareBuilder (0.00s)
PASS
```

### 4.2 测试发现的问题

1. `accessLog.Route`字段记录的值不正确.这里日志应该记录的是`/a/b/*`,而非`*`
2. `accessLog.Host`值没记录上

这里第2个问题是好解决的,只要正常通过HTTP访问的请求,都能拿到这个Host.我们的测试用例由于是通过调用`http.NewRequest()`函数拿到的一个`http.Request`实例,因此`http.Request.Host`字段没有值

重点是第1个问题.

### 4.3 修Bug:命中的路由在日志中记录不正确

这个Bug比较好修.整体思路分为4步:

* step1. 为`node`结构体添加一个**承载该节点对应的全路由**的字段.注意:这里不需要为路由树上的每个节点都赋值该字段
  * 例如:注册路由`/a/b/c`,其实只需为最深层的`c`节点赋值全路由即可.因为我们添加这个字段的目的是为了**在记录日志时能够取到**,而实际上用户如果访问路由`/a`或`/a/b`,根本就命中不了任何路由,也就不用记录日志了
* step2. 注册路由时将全路由写入到节点(确切的说是绑定了`HandleFunc`的节点)中
* step3. 匹配节点时从命中的节点中读取该字段并赋值给`Context`
* step4. 记录日志时从`Context`中读取全路由(其实现在就是从`Context`中读取的全路由,所以这一步不用改任何代码)

#### 4.3.1 为`node`结构体添加用于承载全路径的字段

`node.go`:

```go
// node 路由树的节点
type node struct {
	route         string           // route 当前节点的全路由
	path          string           // path 当前节点的路径
	children      map[string]*node // children 子路由路径到子节点的映射
	wildcardChild *node            // wildcardChild 通配符子节点
	paramChild    *node            // paramChild 参数子节点
	HandleFunc                     // HandleFunc 路由对应的业务逻辑
}
```

#### 4.3.2 将全路由写入到节点中

`router.go`:

```go
func (r *router) addRoute(method string, path string, handleFunc HandleFunc) {
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	root, ok := r.trees[method]
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	if path == "/" {
		if root.HandleFunc != nil {
			panic("web: 路由冲突,重复注册路由 [/] ")
		}
		root.HandleFunc = handleFunc

		// 记录根节点的全路由(实际上就是"/")
		root.route = path
		return
	}

	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	target := root
	for _, segment := range segments {
		if segment == "" {
			panic("web: 路由中不得包含连续的'/'")
		}

		child := target.childOrCreate(segment)
		
		target = child
	}

	if target.HandleFunc != nil {
		panic(fmt.Sprintf("web: 路由冲突,重复注册路由 [%s] ", path))
	}

	target.HandleFunc = handleFunc
	
	// 记录目标节点的全路由
	target.route = path
}
```

注:此处为了便于观看,只保留了记录全路由相关代码的注释,其他注释都删掉了

#### 4.3.3 匹配节点时从命中的节点中读取该字段并赋值给`Context`

`httpServer.go`:

```go
func (s *HTTPServer) serve(ctx *Context) {
	method := ctx.Req.Method
	path := ctx.Req.URL.Path
	targetNode, ok := s.findRoute(method, path)
	if !ok || targetNode.node.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		_, _ = ctx.Resp.Write([]byte("Not Found"))
		return
	}

	ctx.PathParams = targetNode.pathParams

	// 命中节点则将节点的路由设置到上下文中
	ctx.MatchRoute = targetNode.node.route

	targetNode.node.HandleFunc(ctx)
}
```

注:此处为了便于观看,只保留了传递节点全路由给`Context`相关代码的注释,其他注释都删掉了

这里应该传递的是`node.route`而非`node.path`

#### 4.3.4 重新执行测试用例

测试用例的代码没有任何变更,重新测试的结果如下:

```
=== RUN   Test_MiddlewareBuilder
hello, it's me
{"route":"a/b/*","HTTPMethod":"GET","path":"/a/b/c"}
--- PASS: Test_MiddlewareBuilder (0.00s)
PASS
```

可以看到,此时结果符合预期了

### 4.4 以启动服务器的方式编写测试用例

这里我们就只剩下测试`Host`字段是否正常工作了:

`middlewares/access_log/middleware_test.go`:

```go
// Test_MiddlewareBuilderWithServer 以启动服务器的方式测试记录日志中间件
func Test_MiddlewareBuilderWithServer(t *testing.T) {
	// 创建记录日志中间件
	builder := &MiddlewareBuilder{}
	// 注意这里的链式调用 使得在创建中间件的同时就可以设置记录日志的函数
	accessLogMiddleware := builder.SetLogFunc(func(logContent string) {
		fmt.Println(logContent)
	}).Build()

	// 创建中间件Option
	middlewareOption := web.ServerWithMiddleware(accessLogMiddleware)

	// 创建服务器
	// 这里就可以看出Option模式的好处了: 通过不同的Option函数可以设置HTTPServer实例的不同属性
	s := web.NewHTTPServer(middlewareOption)

	// 注册路由
	s.GET("/a/b/*", func(ctx *web.Context) {
		ctx.Resp.Write([]byte("hello, it's me"))
	})

	// 启动服务器
	s.Start(":8092")
}
```

测试访问:

![测试访问](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-686bb2126f31e853cb48fe91881b23fcf8efd9bf%2F%E6%B5%8B%E8%AF%95%E8%AE%BF%E9%97%AE.png?alt=media)

运行结果:

```
=== RUN   Test_MiddlewareBuilderWithServer
{"host":"localhost:8092","route":"a/b/*","HTTPMethod":"GET","path":"/a/b/c"}
```

## PART5. 潜在的问题

我们写的`MiddlewareBuilder.Build()`方法,相当于是一个样板,用户完全可以照猫画虎自己实现一个符合他需求的AccessLog中间件

这是老师和别人在**设计理念上的巨大差异**.在这种地方,用户完全可以自己提供扩展实现,且我们也设计了良好的接口让用户接入(回想`func NewHTTPServer(opts ...Option) *HTTPServer`的函数签名).老师也不愿意花费很多时间去设计精巧的结构和机制,去支持各种千奇百怪的用法.原则只有一个:**用户有特殊需求,用户就自己写**.

**默认提供的实现是给大多数普通用户使用的**,也相当于一个例子,有需要的用户可以参考这个实现写自己的实现.

对于大多数普通用户而言,简单记录一下`accessLogMiddleware`结构体中的字段(主机、命中的路由、请求的uri、请求的HTTP动词)就已经足够用了.更加复杂的、针对特定业务特性的AccessLog,应该自行实现.

在开源领域,也分为两类人:保姆型研发和简洁型研发

保姆型研发:使用者要什么,他们就去设计和实现什么,且实现时会考虑到方方面面的问题.尽可能的为用户着想,兼容用户的各种输入

简洁型研发:只提供主流的实现,但同时提供了扩展接口和例子给使用者.如果使用者有需求,让他们自己去实现.当然,使用者需要自己去学这个扩展接口怎么适配的


# 7.02 Middleware-Trace简介和OpenTelemetry

## PART1. Trace简介

Tracing:踪迹.它记录了从收到请求到返回响应的整个过程.在分布式环境下,**一般代表着请求从Web收到,沿着后续微服务链条传递,得到响应再返回前端的过程**.

![Tracing的概念](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-507bdd48c3173bd140b8a4fffeea0a447ab1ce79%2FTracing%E7%9A%84%E6%A6%82%E5%BF%B5.png?alt=media)

如图所示,这里说的"请求从Web收到",是指BFF层收到.

BFF层:Backend For Frontend层,针对聚合服务的一层,用于为前端页面拼凑来自各个领域服务的数据

在BFF层之前的过程(比如Nginx网关),不会被记录在Tracing中.

通常状况下,BFF层调用聚合服务层,聚合服务调用(多个)领域服务.包括被聚合服务调用到的领域服务,其本身也会调用其他领域服务.

普适的调用链如下:

BFF --调用--> 聚合服务 --调用--> 领域服务1 --调用--> 领域服务2

BFF <--响应-- 聚合服务 <--响应-- 领域服务1 <--响应-- 领域服务2

经过这个完整的调用链后,即可得到一个记录了完整调用过程的trace

### 1.1 链路追踪

![Tracing的示例-1](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-449764db431f5e971fb00aaf745048b1a4e88b25%2FTracing%E7%9A%84%E7%A4%BA%E4%BE%8B-1.png?alt=media)

* step1. BFF层收到请求,调用订单聚合服务
* step2. 订单聚合服务调用风控服务.这里的调用取决于公司的技术选型,有可能是RPC调用,也有可能是HTTP调用
* step3. 风控服务响应订单聚合服务:当笔订单能否继续进行
  * 一般真实的创建订单的过程,非常复杂.以风控服务为例,会判断很多因素:
    * 商家是否为正常商家(商家有无洗钱嫌疑);
    * 订单中的商品是否为正常的商品
    * 买家是否为一个正常的买家(是否存在刷单嫌疑);
* step4. 订单聚合服务调用商品服务
* step5. 商品服务返回商品的定价、商品的库存等商品关键信息
* step6. 订单聚合服务调用促销服务,获取当笔订单可用的优惠券、红包等折扣信息
* step7. 促销服务返回可用的优惠券、红包
* step8. 订单聚合服务计算订单金额(这里的金额包括很多金额:优惠前金额、优惠后金额、实付金额等各种金额)
  * 有些大厂在这个步骤上的实现非常复杂,比如根据价格引擎中配置的计价规则(这个规则是运营人员配置的,很复杂)计算各种商品的价格,最终求出订单的金额
* step9. 订单聚合服务创建订单
  * 这个步骤也很复杂,涉及到扣除库存等细节问题的处理

需要注意的是,trace中不仅包含服务调用信息,而是包含整条链路上的信息,共包括:

* RPC调用
* HTTP调用
* 数据库查询
* 发送消息(例如发送消息至MQ)
* 业务步骤(例如上述步骤的step8)

这些步骤,也可以进一步被细分,分成更加细的span

![Tracing的示例-2](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-7222942247446e2aff78821fcf665cd41d4a2445%2FTracing%E7%9A%84%E7%A4%BA%E4%BE%8B-2.png?alt=media)

注意:图中绿色的`BFF创建订单`和紫色的`订单聚合服务`都叫做span

长方形的宽表示耗时,宽的值越大则表示耗时越长.

需要注意的是,并不一定能在BFF的span内看到所有的调用信息.因为有一些敏感信息(比如图中的风控服务)可能不希望被看到,那么可以设置在风控服务的span内,将一些步骤的链路追踪设置为不可见.

这样一来,在BFF的span内就只能看到风控服务span的总体情况,而看不到内部的细节.

### 1.2 相关概念

* tracer:表示记录trace(踪迹)的实例,一般来说,tracer会有一个对应的接口.可以理解为Factory或Builder的概念,用于构造trace
* span:代表trace中的一段.因此trace本身也可以看做是一个span.span本身是一个层级概念,因此有父子关系.**一个trace的span可以看做是多叉树**

可以看到上图中存在2处空隙.空隙表示的是代码运行的耗时.**如果一个空隙很宽的话,那么说明打点不够详细.需要再在代码中打入一些追踪点**

### 1.3 tracing工具

目前在业界里面,tracing工具还处于百花齐放阶段,有很多开源实现.例如:SkyWalking、Zipkin、Jeager等

#### 1.3.1 第1个问题:需不需要同时支持多种tracing工具?

这个问题的本质是:我们设计的框架,其使用者是谁?

* 给公司内部使用:那么就做成仅支持公司使用的、特定的tracing工具即可.例如公司用zipkin,那就支持zipkin就行了;甚至公司有自研的tracing工具,那么就支持这个自研的tracing工具即可
* 设计开源框架:开源框架的使用者,那就是使用任何tracing工具都有可能了.因此需要支持主流的tracing工具

那么问题来了,该怎么支持这些开源工具?

#### 1.3.2 第2个问题:怎么支持这些开源工具?

![Tracing工具的支持思路](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3dcbcd468292707da4c1449e9dede96aed8f5016%2FTracing%E5%B7%A5%E5%85%B7%E7%9A%84%E6%94%AF%E6%8C%81%E6%80%9D%E8%B7%AF.png?alt=media)

总体的思路其实就是两种:

1. 为每个tracing工具写一个Middleware(如上图的上半部分)

   这个思路的缺点在于:

   1. 每个中间件都会和对应的tracing工具强耦合
   2. 框架会对这些具体的tracing工具产生依赖关系
2. 定义一个统一的API,框架只和这个API交互.这个API允许用户注入自己的tracing工具的实现(如下图的上半部分)

很明显后一种思路是抽象层级更高、更通用的解决方案.因此我们采用这种思路来设计我们框架中的Tracing部分

### 1.4 自定义API

一般来讲,如果一个中间件设计者想要摆脱对任何第三方的依赖,都需要采用**定义自己的API**的方式来达到这个目的,常见的有:

* 定义Log API
* 定义Config API
* 定义Tracing API
* 定义Metrics API

#### 1.4.1 为什么我们没有设计设计Log API和Config API?

那么问题来了:我们在设计框架的时候,并没有设计Log API和Config API啊?

这里以`HTTPServer.Start()`方法和`NewHTTPServer()`函数举例说明.

来看我们的`HTTPServer.Start()`方法的方法签名:

```go
func (s *HTTPServer) Start(addr string) error
```

调用`HTTPServer.Start()`方法时,我们作为框架的设计者,**要求用户把参数传进来**.至于这个参数是用户通过何种途径得到的,框架的设计者并不关心.

至于这个参数到底是怎么来的,那可能性就太多了:

* 从配置文件中读取的
* 从环境变量中读取的
* 从程序启动时的参数中读取的

我们作为框架的设计者,如果要去管"参数怎么来的"这件事,那根本是管不完的,而且管这个事儿的意义和含金量都不大.

不如提供一个**编程接口**,像`HTTPServer.Start()`方法那样设计:框架的设计者不关心使用者的参数从何种途径获得,只关心用户传到方法中的参数值即可.

和这种思路相反的思路是**耦合式开发**.我们以"创建`HTTPServer`实例"这个功能为例来说明:

```go
// NewHTTPServerWithConfig 从给定的配置文件中读取配置项 根据配置项创建HTTP服务器
func NewHTTPServerWithConfig(configFilePath string) *HTTPServer {
	// 在函数体内读取配置文件 解析为一个配置对象
	// 然后根据配置对象创建 HTTPServer 实例
	return nil
}
```

按照这种设计形态,则必然需要设计一个`Config`结构体.

如果采取这种设计思想,面临的问题会很多,这里列举几个:

1. 参数`configFilePath`,是相对路径还是绝对路径?
   * 如果是相对路径,是相对于哪个路径的路径?
     * 让使用者传递相对路径,是比较容易出错的.因为使用者很有可能不知道这个函数中定义的相对路径,相对的是哪个路径.可能有人会直觉性地认为相对的是工程根目录,但在稍微复杂一些的运维环境中,研发人员真的未必知道工程根目录的路径是什么
2. 配置文件格式问题
   * 由于需要读取不同格式(json/yaml/toml/xml/envfile)的配置文件,因此不可避免地会引入类似[viper](https://github.com/spf13/viper)这种第三方库,这就对第三方产生了依赖
   * 也是因为配置文件格式的多样性,框架的设计者还需要实现针对不同格式的序列化与反序列化.这也是一个复杂且恶心的过程

但实际上设计者直接把这个权利交给用户就好了,根本没必要去实现这些**父母式编程**的功能.只实现**编程接口**就可以了

#### 1.4.2 如何摆脱第三方依赖?

![自定义API](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-83492737a99a3dceef10f85d8f1b0c3d8407dc74%2F%E8%87%AA%E5%AE%9A%E4%B9%89API.png?alt=media)

如上图示,想要彻底摆脱第三方依赖,那就得定义出自己的API.换言之就是我们设计的框架核心是稳定的、不直接依赖于第三方库的.使用者在初始化框架的过程中,将API对应的实现通过框架暴露给外部的接口注入进来即可.

**a. 自定义API的优点**

这种设计的好处就在于**框架的内核稳定**,任何第三方库的变更都不会影响到框架

**b. 自定义API的缺点**

* 过度设计:有些场景下即使定义了API,也只会有1个默认实现(比如[`echo.Context`接口](https://github.com/labstack/echo/blob/584cb85a6b749846ac26a8cd151244ab281f2abc/context.go#L19),它仅有1个实现[`echo.context`](https://github.com/labstack/echo/blob/584cb85a6b749846ac26a8cd151244ab281f2abc/context.go#L200))
* API设计的并不怎么样
  * 当然,这并不是自定义API这种设计形态的缺点,而是写代码的人(就是我自己)自身的缺点.

评判一个API设计的好不好,大体上从以下几个方面就能看出来:

1. 易用性:

   框架的使用者通过我定义的API接入一个实现,接入的过程是否容易?
2. 扩展性:

   你设计的API不可能不发生任何的变更,随着第三方库的功能扩展或其他原因,你设计的用于接入第三方的API,迟早会有需要变更的一天.当变更来到时,你当初的设计能否让变更对第三方库的影响降到最低?

   TODO:我对这句话没概念

**忠告:如果你设计不好这种API,那就不要采用自定义API的设计方案**!

在公司可以这么干,因为干完了能拿来吹牛逼;搞开源不能这么搞,因为大概率你这么搞完了事儿就搞砸了,你真就成个睾丸了.

### 1.5 采用OpenTelemetry API

![OpenTelemetryAPI](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e3900dedfed9adeb10dfaa53435f2b29038a19ed%2FOpenTelemetryAPI.png?alt=media)

如上图示,最终我们决定依赖OpenTelemetry API来完成各种链路追踪中间件的接入.理由很简单:**我们定义的API肯定不如OpenTelemetry API**.因此采用OpenTelemetry API作为抽象层.

图中的OpenTelemetryMiddleware,实际上就是1.3.2小节图中的TracingMiddleware.然后就可以注入OpenTelemetryMiddleware的不同实现了

那么问题来了,OpenTelemetry是啥?

## PART2. OpenTelemetry简介

[OpenTelemetry](https://opentelemetry.io/)是OpenTracing和OpenCensus合并而来

![OpenTelemetry历史](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-a995bd836e14588b2eb76a020635046b3592fca8%2FOpenTelemetry%E5%8E%86%E5%8F%B2.png?alt=media)

* OpenTelemetry同时支持了**logging、tracing和metrics**
* OpenTelemetry**提供了各种语言的SDK**
* OpenTelemetry**适配了各种开源链路追踪中间件**,如Zipkin、Jeager、Prometheus

![OpenTelemetry优势](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-90e948e072a79463bc02fc087ff07f9d25626b39%2FOpenTelemetry%E4%BC%98%E5%8A%BF.png?alt=media)

总而言之言而总之,OpenTelemetry是新时代的可观测性平台

### 2.1 OpenTelemetry GO SDK入门

[文档地址](https://opentelemetry.io/docs/instrumentation/go/)

![SDK调用](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e7b7c8835aa4c7b53f3e17c7ec4d80694e74e88e%2FSDK%E8%B0%83%E7%94%A8.png?alt=media)

注:这个图中的代码我也没写过,就是理解一下图中每个API的含义

#### 2.1.1 TracerProvider

TracerProvider:**用于构造tracer实例**.Provider在平时开发中也很常见,有时可以看作是轻量级的工厂模式或轻量级的生成器模式.

通常Provider会提供一些缓存机制.比如图中的`otel.GetTracerProvider()`这行代码,调用过这个API之后,后续如果再调用`otel.GetTracerProvider()`,你拿到的TracerProvider和之前调用`otel.GetTracerProvider()`拿到的TracerProvider是同一个TracerProvider

#### 2.1.2 tracer

tracer:追踪者,用于构造trace

构造tracer需要一个`instrumentationName`,一般来说就是你构造tracer的地方的包名(保证唯一即可).例如图中的`Tracer("gitbub.com/flycash/geekbang-middleware")`.这里的`"gitbub.com/flycash/geekbang-middleware"`就是`instrumentationName`

#### 2.1.3 span

span:调用tracer上的`Start()`方法.如果传入的context里面已经有一个span了,那么新创建的span就是老的span的儿子.span要记住调用`End()`方法

图中的

```go
ctx，span := tracer.Start(ctx, "opentelemetry-demo", 
	trace.WithAttributes(attribute.String("version"，"1")))
```

即为调用了tracer上的`Start()`方法.

这里需要注意的是,如果调用`tracer.Start()`方法时传入的context(我们将这个context命名为contextA)已经和一个span(我们将这个span命名为spanB)绑定过的话,则使用contextA调用`tracer.Start()`方法时返回的span(我们将这个span命名为spanB)是spanA的儿子,二者构成层级关系

图中的`span.End()`,其含义为结束这个span,标志着这个span的生命周期结束

### 2.2 OpenTelemetry与Zipkin和Jeager的结合

![OpenTelemetry与Zipkin结合](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3df6d8cdac1d4c43ced110f952371d09d62d7856%2FOpenTelemetry%E4%B8%8EZipkin%E7%BB%93%E5%90%88.png?alt=media)

![OpenTelemetry与Jeager结合](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3a6f7717375a581bf8ed66a1397317e5048cc879%2FOpenTelemetry%E4%B8%8EJeager%E7%BB%93%E5%90%88.png?alt=media)

上图为向OpenTelemetry注入Zipkin的实现;

下图为向OpenTelemetry注入Jeager的实现;

具体的代码都不用记,重点是看这两段代码的相似性:

* 都是先构建各自的exporter
* 然后再将这个exporter转换成对应的TraceProvider
* 最后调用`otel.SetTracerProvider()`方法,将TraceProvider注入到OpenTelemetry中

可以把这个注入的过程理解为一个适配器模式:各种exporter要去适配TraceProvider.

至于具体怎么创建exporter,这个完全可以抄代码.不用记,就抄图中的代码即可

## PART3. OpenTelemetry Middleware

### 3.1 Builder模式

本节课工程结构如下:

```
(base) yanglei@yuanhong 02-tracing % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│   └── access_log
│       ├── accessLog.go
│       ├── accessLog_test.go
│       └── middlewareBuilder.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

2 directories, 20 files
```

和写accessLog时一样,也是先定义出Builder模式的基本结构:

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"web"
)

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

### 3.2 初始化Tracer

#### 3.2.1 将Tracer设置为一个公有字段的方案

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

#### 3.2.2 将Tracer设置为一个私有字段的方案

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel/trace"
)

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	tracer trace.Tracer // tracer 追踪器
}

// NewMiddlewareBuilder 创建中间件构建器
func NewMiddlewareBuilder(tracer trace.Tracer) *MiddlewareBuilder {
	return &MiddlewareBuilder{
		tracer: tracer,
	}
}
```

也可以将这个字段的属性设置为私有,在实例化`MiddlewareBuilder`时要求框架使用者必须传入一个`trace.Tracer`

我们在这里采用公有字段的方案.因为通常情况下框架的使用者会创建一个全局的Tracer,所以就没必要再让他们把一个创建好的实例传到`open_telemetry`包里来了

### 3.3 创建span并记录数据

#### 3.3.1 创建span并修改span的名称

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			// step1. 创建span
			// 1. Tracer.Start()方法的第1个参数是一个 context.Context 接口的实现
			// 而我们的 web.Context 并没有实现 context.Context 接口 (其实在这里拿到的是 context.Background() )
			// 2. Tracer.Start()方法的第2个参数是一个 span 的名称 通常以请求命中的路由作为 span 的名称
			// 但此时还不知道这个请求是否能命中路由 所以使用 "unknown" 作为 span 的名称
			reqCtx, span := m.Tracer.Start(ctx.Req.Context(), "unknown")
			// 4. 调用完成后要关闭span
			defer span.End()

			next(ctx)
			// 3. 只有当请求命中路由(实际上在这里已经执行完了对应的 web.HandleFunc 了)后
			// 才能确定请求命中的路由 因此在执行完 HandleFunc 后 再把span的名称改成请求命中的路由
			if ctx.MatchRoute != "" {
				span.SetName(ctx.MatchRoute)
			}
		}
	}
}
```

这里有3个需要注意的地方:

1. `Tracer.Start()`方法接收的第1个参数是`context.Context`接口的实现,而非是我们自己设计的`web.Context`.因此需要拿请求的上下文作为此处的参数
2. `Tracer.Start()`方法接收的第2个参数是一个span的名称,通常以请求命中的路由作为span的名称.可是在创建span时,还不确定当前的请求能否命中路由,所以先用`unknown`替代
3. 当请求命中了路由,并执行完对应的HandleFunc后,就能够确定命中的路由了.这时要将span的名称修改为请求命中的路由.这里需要注意的是,如果没有找到命中的路由,说明就404了,这种情况下同样不需要修改span的名称.换言之,仅在确定命中了路由时,才需要修改span的名称
4. 调用完成后要关闭span

#### 3.3.2 记录数据

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			// step1. 创建span
			// 1. Tracer.Start()方法的第1个参数是一个 context.Context 接口的实现
			// 而我们的 web.Context 并没有实现 context.Context 接口 (其实在这里拿到的是 context.Background() )
			// 2. Tracer.Start()方法的第2个参数是一个 span 的名称 通常以请求命中的路由作为 span 的名称
			// 但此时还不知道这个请求是否能命中路由 所以使用 "unknown" 作为 span 的名称
			_, span := m.Tracer.Start(ctx.Req.Context(), "unknown")
			// 4. 调用完成后要关闭span
			defer span.End()

			// step2. 设置span的标签(就是记录数据)
			attributes := []attribute.KeyValue{
				// 请求的HTTP动词
				attribute.String("http.method", ctx.Req.Method),
				// 请求的url
				attribute.String("http.url", ctx.Req.URL.String()),
				// 请求的scheme (http/https)
				attribute.String("http.scheme", ctx.Req.URL.Scheme),
				// 请求的host
				attribute.String("http.host", ctx.Req.URL.Host),
			}
			span.SetAttributes(attributes...)

			next(ctx)

			// 3. 只有当请求命中路由(实际上在这里已经执行完了对应的 web.HandleFunc 了)后
			// 才能确定请求命中的路由 因此在执行完 HandleFunc 后 再把span的名称改成请求命中的路由
			if ctx.MatchRoute != "" {
				span.SetName(ctx.MatchRoute)
			}
		}
	}
}
```

#### 3.3.3 与上下游结合

截至目前我们的实现都是有一个边界的:仅在进程内记录了span.那么问题来了:如果现在的场景是跨进程通信的,且我们的客户端也有其自己的span,该如何把我们创建的span和客户端的span结合在一起?

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			// step3. 尝试与客户端的span建立父子关系
			// 1. 从请求头中获取traceId和spanId (客户端的traceId和spanId是放在HTTP请求头中的)
			reqCtx := ctx.Req.Context()
			// Tips: 其实这个HeaderCarrier就是一个map[string][]string 跟request.Header是一样的
			carrier := propagation.HeaderCarrier(ctx.Req.Header)
			// 2. 基于客户端的traceId和spanId 重新封装成context.Context
			reqCtx = otel.GetTextMapPropagator().Extract(reqCtx, carrier)

			// step1. 创建span
			// 1. Tracer.Start()方法的第1个参数是一个 context.Context 接口的实现
			// 而我们的 web.Context 并没有实现 context.Context 接口 (其实在这里拿到的是 context.Background() )
			// 2. Tracer.Start()方法的第2个参数是一个 span 的名称 通常以请求命中的路由作为 span 的名称
			// 但此时还不知道这个请求是否能命中路由 所以使用 "unknown" 作为 span 的名称
			// Tips: 此时拿到的就是基于客户端的traceId和spanId封装过的context.Context
			_, span := m.Tracer.Start(reqCtx, "unknown")
			// 4. 调用完成后要关闭span
			defer span.End()

			// step2. 设置span的标签(就是记录数据)
			attributes := []attribute.KeyValue{
				// 请求的HTTP动词
				attribute.String("http.method", ctx.Req.Method),
				// 请求的url
				attribute.String("http.url", ctx.Req.URL.String()),
				// 请求的scheme (http/https)
				attribute.String("http.scheme", ctx.Req.URL.Scheme),
				// 请求的host
				attribute.String("http.host", ctx.Req.URL.Host),
			}
			span.SetAttributes(attributes...)

			next(ctx)

			// 3. 只有当请求命中路由(实际上在这里已经执行完了对应的 web.HandleFunc 了)后
			// 才能确定请求命中的路由 因此在执行完 HandleFunc 后 再把span的名称改成请求命中的路由
			if ctx.MatchRoute != "" {
				span.SetName(ctx.MatchRoute)
			}
		}
	}
}
```

这里有3个需要注意的地方:

1. 从HTTP请求头中获取客户端的traceId和spanId
   * 这里需要将`request.Header`转换为一个`propagation.HeaderCarrier`(其实二者本质上都是`map[string][]string`)
2. 获取客户端的traceId和spanId后,会拿到一个基于客户端的`context.Context`重新封装过`context.Context`
3. 后续创建span时,要基于这个重新封装过的`context.Context`来创建.这样才能让我们创建的span和客户端的span构成父子关系

TODO: 其实这块我也不是很懂,但是我隐隐感觉,基本上80%的代码照着抄就行


# 7.03 Middleware-OpenTelemetry测试

本节课工程结构如下:

```
(base) yanglei@yuanhong 03-tracingTest % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│   ├── access_log
│   │   ├── accessLog.go
│   │   ├── accessLog_test.go
│   │   └── middlewareBuilder.go
│   └── open_telemetry
│       └── middlewareBuilder.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

3 directories, 21 files
```

## PART1. 编写测试用例

想要完全的测试一个tracer是否正确存储其实是比较困难的,这里不太好办的点在于:你不太容易把span从OpenTelemetry中取出来看,进而确保这个span上报到了zipkin或jeager中.所以这里的测试思路只能是:写个HandleFunc,在这个HandleFunc中记录一些span,最后去zipkin或jeager中查看.

就像我们平时对一个CRUD的测试一样,写一个INSERT语句,测试之后只要看数据是否落盘即可.

### 1.1 创建Middleware和Server

`middlewares/open_telemetry/middleware_test.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"testing"
	"time"
	"web"
)

// Test_MiddlewareBuilder 以创建 web.HandleFunc 的形式,在这个HandleFunc
// 中创建几个具有父子关系的span,并最终在第三方的tracing平台中查看这些span
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件
	tracer := otel.GetTracerProvider().Tracer(instrumentationName)
	builder := &MiddlewareBuilder{
		Tracer: tracer,
	}
	tracingMiddleware := builder.Build()

	// 创建中间件Option
	middlewareOption := web.ServerWithMiddleware(tracingMiddleware)

	// 创建服务器
	s := web.NewHTTPServer(middlewareOption)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		// 创建第1层span
		firstLayerContext1, firstLayerSpan1 := tracer.Start(ctx.Req.Context(), "first_layer_1")
		_, firstLayerSpan2 := tracer.Start(ctx.Req.Context(), "first_layer_2")

		// 创建第2层span 第2层的span是第1层span的子span
		secondLayerContext, secondLayerSpan := tracer.Start(firstLayerContext1, "second_layer")
		// 暂停1s 目的在于: 使得第2层span的开始时间与第1层span的开始时间有较为明显的时间差
		time.Sleep(time.Second)
		// 这里的defer secondLayerSpan.End()其实是可以不写的,因为在第一层span结束的时候,第二层span也会结束

		// 创建第3层的span 第3层的span是第2层span的子span
		_, thirdLayerSpan1 := tracer.Start(secondLayerContext, "third_layer_1")
		// 暂停100ms 先关闭第3层的第1个span
		time.Sleep(100 * time.Millisecond)
		thirdLayerSpan1.End()
		_, thirdLayerSpan2 := tracer.Start(secondLayerContext, "third_layer_2")
		// 暂停300ms 再关闭第3层的第2个span
		// 这样做是为了让第3层的2个span之间是一个有明显时间差的关系
		time.Sleep(300 * time.Millisecond)
		thirdLayerSpan2.End()

		// 再关闭第2层的span
		secondLayerSpan.End()

		// 最后关闭第1层的span
		firstLayerSpan1.End()
		firstLayerSpan2.End()

		ctx.Resp.Write([]byte("hello, world"))
		ctx.Resp.WriteHeader(http.StatusOK)
	}

	// 注册路由并启动服务器
	s.GET("/user", handleFunc)
	s.Start(":8092")
}
```

这里要注意,各层级的span之间是有父子关系的,创建的时候必然是先创建父span,再基于父span创建子span;关闭的时候则相反,需要先关闭子span再关闭父span.如果关闭的时候先关父span,则子span会随之关闭.这里我们为了观察各层级span之间的父子关系,就不把关闭操作写的太复杂了.

### 1.2 初始化Tracing

#### 1.2.1 初始化zipkin

`middlewares/open_telemetry/middleware_test.go`:

```go
func initZipkin(t *testing.T) {
	// 要注意这个端口，和 docker-compose 中的保持一致
	exporter, err := zipkin.New(
		"http://localhost:19411/api/v2/spans",
		zipkin.WithLogger(log.New(os.Stderr, "opentelemetry-demo", log.Ldate|log.Ltime|log.Llongfile)),
	)
	if err != nil {
		t.Fatal(err)
	}

	batcher := sdktrace.NewBatchSpanProcessor(exporter)
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithSpanProcessor(batcher),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("opentelemetry-demo"),
		)),
	)
	otel.SetTracerProvider(tp)
}
```

#### 1.2.2 初始化jeager

`middlewares/open_telemetry/middleware_test.go`:

```go
func initJeager(t *testing.T) {
	url := "http://localhost:14268/api/traces"
	exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url)))
	if err != nil {
		t.Fatal(err)
	}
	tp := sdktrace.NewTracerProvider(
		// Always be sure to batch in production.
		sdktrace.WithBatcher(exp),
		// Record information about this application in a Resource.
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceNameKey.String("opentelemetry-demo"),
			attribute.String("environment", "dev"),
			attribute.Int64("ID", 1),
		)),
	)

	otel.SetTracerProvider(tp)
}
```

注:以上两段代码都是直接粘过来的,因为我觉得这种代码意义不是很大,而且我确实也明白不了这里边都是啥意思,有需要直接GPT查应该就行

### 1.3 设置OpenTelemetry的Tracer Provider

实际上就是在HandleFunc中调用以上两个函数中的任意一个即可,因为这2个函数中都调用了`otel.SetTracerProvider()`,设置了全局的 Tracer提供器.

`middlewares/open_telemetry/middleware_test.go`:

```go
// Test_MiddlewareBuilder 以创建 web.HandleFunc 的形式,在这个HandleFunc
// 中创建几个具有父子关系的span,并最终在第三方的tracing平台中查看这些span
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件
	tracer := otel.GetTracerProvider().Tracer(instrumentationName)
	builder := &MiddlewareBuilder{
		Tracer: tracer,
	}
	tracingMiddleware := builder.Build()

	// 创建中间件Option
	middlewareOption := web.ServerWithMiddleware(tracingMiddleware)

	// 创建服务器
	s := web.NewHTTPServer(middlewareOption)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		// 创建第1层span
		firstLayerContext1, firstLayerSpan1 := tracer.Start(ctx.Req.Context(), "first_layer_1")
		_, firstLayerSpan2 := tracer.Start(ctx.Req.Context(), "first_layer_2")

		// 创建第2层span 第2层的span是第1层span的子span
		secondLayerContext, secondLayerSpan := tracer.Start(firstLayerContext1, "second_layer")
		// 暂停1s 目的在于: 使得第2层span的开始时间与第1层span的开始时间有较为明显的时间差
		time.Sleep(time.Second)
		// 这里的defer secondLayerSpan.End()其实是可以不写的,因为在第一层span结束的时候,第二层span也会结束

		// 创建第3层的span 第3层的span是第2层span的子span
		_, thirdLayerSpan1 := tracer.Start(secondLayerContext, "third_layer_1")
		// 暂停100ms 先关闭第3层的第1个span
		time.Sleep(100 * time.Millisecond)
		thirdLayerSpan1.End()
		_, thirdLayerSpan2 := tracer.Start(secondLayerContext, "third_layer_2")
		// 暂停300ms 再关闭第3层的第2个span
		// 这样做是为了让第3层的2个span之间是一个有明显时间差的关系
		time.Sleep(300 * time.Millisecond)
		thirdLayerSpan2.End()

		// 再关闭第2层的span
		secondLayerSpan.End()

		// 最后关闭第1层的span
		firstLayerSpan1.End()
		firstLayerSpan2.End()

		ctx.Resp.Write([]byte("hello, world"))
		ctx.Resp.WriteHeader(http.StatusOK)
	}

	// 设置TracerProvider
	initZipkin(t)

	// 注册路由并启动服务器
	s.GET("/user", handleFunc)
	s.Start(":8092")
}
```

## PART2. 启动zipkin

`middlewares/open_telemetry/docker-compose.yaml`:

```yaml
services:
  # Zipkin
  zipkin-all-in-one:
    image: openzipkin/zipkin:latest
    ports:
      - "19411:9411"
```

## PART3. 测试

请求`http://localhost:8092/user`后,查看zipkin如下:

![问题1-请求的span没有和handleFunc内的span关联](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-e22b4f84def4997e013a5d3992b7b0c890dc521c%2F%E9%97%AE%E9%A2%981-%E8%AF%B7%E6%B1%82%E7%9A%84span%E6%B2%A1%E6%9C%89%E5%92%8ChandleFunc%E5%86%85%E7%9A%84span%E5%85%B3%E8%81%94.png?alt=media)

这里是不太符合预期的,我们是希望first\_layer\_1和first\_layer\_2挂在/user下的.

![firstLayerSpan的父子关系](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-79c65a1da62267a0a6b80eb7484fc1533c435889%2FfirstLayerSpan%E7%9A%84%E7%88%B6%E5%AD%90%E5%85%B3%E7%B3%BB.png?alt=media)

## PART4. 修Bug

### 4.1 修复层级关系不正确的问题

#### 4.1.1 原因

![没有建立层级关系的原因](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-a01bd9e9f79d6fa420136c2c24f4233f27abea91%2F%E6%B2%A1%E6%9C%89%E5%BB%BA%E7%AB%8B%E5%B1%82%E7%BA%A7%E5%85%B3%E7%B3%BB%E7%9A%84%E5%8E%9F%E5%9B%A0.png?alt=media)

换言之,在HandleFunc中拿到的`ctx.Req.Context`,和在中间件中创建的、和span创建了绑定关系的`context.Context`不是一个`Context`

#### 4.1.2 修复

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			// step3. 尝试与客户端的span建立父子关系
			// 1. 从请求头中获取traceId和spanId (客户端的traceId和spanId是放在HTTP请求头中的)
			reqCtx := ctx.Req.Context()
			// Tips: 其实这个HeaderCarrier就是一个map[string][]string 跟request.Header是一样的
			carrier := propagation.HeaderCarrier(ctx.Req.Header)
			// 2. 基于客户端的traceId和spanId 重新封装成context.Context
			reqCtx = otel.GetTextMapPropagator().Extract(reqCtx, carrier)

			// step1. 创建span
			reqCtx, span := m.Tracer.Start(reqCtx, "unknown")
			// 4. 调用完成后要关闭span
			defer span.End()

			// step2. 设置span的标签(就是记录数据)
			attributes := []attribute.KeyValue{
				// 请求的HTTP动词
				attribute.String("http.method", ctx.Req.Method),
				// 请求的url
				attribute.String("http.url", ctx.Req.URL.String()),
				// 请求的scheme (http/https)
				attribute.String("http.scheme", ctx.Req.URL.Scheme),
				// 请求的host
				attribute.String("http.host", ctx.Req.URL.Host),
			}
			span.SetAttributes(attributes...)

			// step4. 将和span建立了关系的context.Context重新封装到http.Request中
			ctx.Req = ctx.Req.WithContext(reqCtx)

			next(ctx)

			// 3. 只有当请求命中路由(实际上在这里已经执行完了对应的 web.HandleFunc 了)后
			// 才能确定请求命中的路由 因此在执行完 HandleFunc 后 再把span的名称改成请求命中的路由
			if ctx.MatchRoute != "" {
				span.SetName(ctx.MatchRoute)
			}
		}
	}
}
```

这里需要注意的地方是:`http.Request.WithContext()`方法的实现原理是复制原来的`http.Request`实例到一个新创建的`http.Request`实例上,然后修改这个新创建的`http.Request`实例的`ctx`字段,因此性能会比较差

```go
func (r *Request) WithContext(ctx context.Context) *Request {
	if ctx == nil {
		panic("nil context")
	}
	r2 := new(Request)
	*r2 = *r
	r2.ctx = ctx
	return r2
}
```

如果要是想直接创建一个`http.Request`实例,则应该使用`http.NewRequestWithContext()`函数.

当然也有其他方案,比如给`web.Context`加一个`context.Context`,所有关于请求的`context.Context`都读写这个字段:

在`Context`上添加字段:

`context.go`:

```go
// Context HandleFunc的上下文
type Context struct {
	Req            *http.Request       // Req 请求
	Resp           http.ResponseWriter // Resp 响应
	PathParams     map[string]string   // PathParams 路径参数名值对
	queryValues    url.Values          // queryValues 查询参数名值对
	cookieSameSite http.SameSite       // cookieSameSite cookie的SameSite属性 即同源策略
	MatchRoute     string              // MatchRoute 命中的路由
	Ctx            context.Context     // Ctx 上下文
}
```

中间件中写入到`Context.Ctx`:

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			// step3. 尝试与客户端的span建立父子关系
			// 1. 从请求头中获取traceId和spanId (客户端的traceId和spanId是放在HTTP请求头中的)
			reqCtx := ctx.Req.Context()
			// Tips: 其实这个HeaderCarrier就是一个map[string][]string 跟request.Header是一样的
			carrier := propagation.HeaderCarrier(ctx.Req.Header)
			// 2. 基于客户端的traceId和spanId 重新封装成context.Context
			reqCtx = otel.GetTextMapPropagator().Extract(reqCtx, carrier)

			// step1. 创建span
			reqCtx, span := m.Tracer.Start(reqCtx, "unknown")
			// 4. 调用完成后要关闭span
			defer span.End()

			// step2. 设置span的标签(就是记录数据)
			attributes := []attribute.KeyValue{
				// 请求的HTTP动词
				attribute.String("http.method", ctx.Req.Method),
				// 请求的url
				attribute.String("http.url", ctx.Req.URL.String()),
				// 请求的scheme (http/https)
				attribute.String("http.scheme", ctx.Req.URL.Scheme),
				// 请求的host
				attribute.String("http.host", ctx.Req.URL.Host),
			}
			span.SetAttributes(attributes...)

			// step4. 将和span建立了关系的context.Context重新封装到http.Request中
			ctx.Ctx = reqCtx

			next(ctx)

			// 3. 只有当请求命中路由(实际上在这里已经执行完了对应的 web.HandleFunc 了)后
			// 才能确定请求命中的路由 因此在执行完 HandleFunc 后 再把span的名称改成请求命中的路由
			if ctx.MatchRoute != "" {
				span.SetName(ctx.MatchRoute)
			}
		}
	}
}
```

HandleFunc中使用`Context.Ctx`:

`middlewares/open_telemetry/middleware_test.go`:

```go
// Test_MiddlewareBuilder 以创建 web.HandleFunc 的形式,在这个HandleFunc
// 中创建几个具有父子关系的span,并最终在第三方的tracing平台中查看这些span
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件
	tracer := otel.GetTracerProvider().Tracer(instrumentationName)
	builder := &MiddlewareBuilder{
		Tracer: tracer,
	}
	tracingMiddleware := builder.Build()

	// 创建中间件Option
	middlewareOption := web.ServerWithMiddleware(tracingMiddleware)

	// 创建服务器
	s := web.NewHTTPServer(middlewareOption)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		// 创建第1层span
		firstLayerContext1, firstLayerSpan1 := tracer.Start(ctx.Ctx, "first_layer_1")
		_, firstLayerSpan2 := tracer.Start(ctx.Ctx, "first_layer_2")

		// 创建第2层span 第2层的span是第1层span的子span
		secondLayerContext, secondLayerSpan := tracer.Start(firstLayerContext1, "second_layer")
		// 暂停1s 目的在于: 使得第2层span的开始时间与第1层span的开始时间有较为明显的时间差
		time.Sleep(time.Second)
		// 这里的defer secondLayerSpan.End()其实是可以不写的,因为在第一层span结束的时候,第二层span也会结束

		// 创建第3层的span 第3层的span是第2层span的子span
		_, thirdLayerSpan1 := tracer.Start(secondLayerContext, "third_layer_1")
		// 暂停100ms 先关闭第3层的第1个span
		time.Sleep(100 * time.Millisecond)
		thirdLayerSpan1.End()
		_, thirdLayerSpan2 := tracer.Start(secondLayerContext, "third_layer_2")
		// 暂停300ms 再关闭第3层的第2个span
		// 这样做是为了让第3层的2个span之间是一个有明显时间差的关系
		time.Sleep(300 * time.Millisecond)
		thirdLayerSpan2.End()

		// 再关闭第2层的span
		secondLayerSpan.End()

		// 最后关闭第1层的span
		firstLayerSpan1.End()
		firstLayerSpan2.End()

		ctx.Resp.Write([]byte("hello, world"))
	}

	// 设置TracerProvider
	initZipkin(t)

	// 注册路由并启动服务器
	s.GET("/user", handleFunc)
	s.Start(":8092")
}
```

但这个方案其实问题也比较严重,虽然它没有太大的性能问题,但它造成了一个令使用者困惑的问题:你这个框架的`Context`中有2个地方提供了`context.Context`:

* `Context.Req.Context()`
* `Context.Ctx`

使用者也会疑惑:我到底应该用哪个?

所以最好还是别让使用者有这种选择权了,一则对于我们也不利,我们要维护2个地方;再则对于使用者也不利,使用者也会产生疑惑

#### 4.1.3 修复结果

![修复结果](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-fe524867326d84986c92353391e62c747caebfbc%2F%E4%BF%AE%E5%A4%8D%E7%BB%93%E6%9E%9C.png?alt=media)

![修复后的span父子关系](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-83cddf98bdb6bb8e86d616917220c845c2b7b892%2F%E4%BF%AE%E5%A4%8D%E5%90%8E%E7%9A%84span%E7%88%B6%E5%AD%90%E5%85%B3%E7%B3%BB.png?alt=media)

## PART5. 缺陷

![缺陷-span的标签中没有响应码](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-69f6db4543d2f2c3bf44580ab593ee94de805bde%2F%E7%BC%BA%E9%99%B7-span%E7%9A%84%E6%A0%87%E7%AD%BE%E4%B8%AD%E6%B2%A1%E6%9C%89%E5%93%8D%E5%BA%94%E7%A0%81.png?alt=media)

从上图中可以看出,我们的span没有记录响应码.这是一个需要修复的缺陷

### 5.1 记录响应码的代码在哪里加?

![context中没有响应码](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-cf1bba3b977c1c9e7af357077dd128640c1587e0%2Fcontext%E4%B8%AD%E6%B2%A1%E6%9C%89%E5%93%8D%E5%BA%94%E7%A0%81.png?alt=media)

加肯定是在中间件中调完`next()`加,可问题是:现在`Context`里没记录响应码

### 5.2 Context中记录响应码

#### 5.2.1 实现思路

这里我们就顺便把响应数据也记录下来了,因为后边设计其他中间件时,肯定还会遇到需要响应数据的中间件

首先,不要想着从`http.ResponseWriter`(`ctx.Resp`)接口中直接拿,因为它没有提供获取响应码的方法

其次,类型转换也不行.这里我们假定你有一个`MyResponseWriter`类,实现了`http.ResponseWriter`接口,可能你会想到如下的代码:

![强制类型转换并取状态码](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-54f4da7330e485377a35e7098b2a18e13cb88f07%2F%E5%BC%BA%E5%88%B6%E7%B1%BB%E5%9E%8B%E8%BD%AC%E6%8D%A2%E5%B9%B6%E5%8F%96%E7%8A%B6%E6%80%81%E7%A0%81.png?alt=media)

还是不行:因为你在这里无法确定你要把一个什么类型转换为你自己实现的`MyResponseWriter`.有可能是`http`包内置的类,还有可能是使用者通过写中间件的方式修改过的类,你根本无法确保这里的类型转换一定会成功

那最后只剩下1条路了:单独在`Context`上创建字段记录响应码和相应数据

`context.go`:

```go
// Context HandleFunc的上下文
type Context struct {
	Req            *http.Request       // Req 请求
	Resp           http.ResponseWriter // Resp 响应
	PathParams     map[string]string   // PathParams 路径参数名值对
	queryValues    url.Values          // queryValues 查询参数名值对
	cookieSameSite http.SameSite       // cookieSameSite cookie的SameSite属性 即同源策略
	MatchRoute     string              // MatchRoute 命中的路由
	RespData       []byte              // RespData 响应数据 主要是给中间件使用
	RespStatusCode int                 // RespStatusCode 响应状态码 主要是给中间件使用
}
```

当然,这个方案也是有问题的:如果用户还是使用`ctx.Resp.Write()`和`ctx.Resp.WriteHeader()`这种关于`http.ResponseWriter`的API,那么你的中间件是无法从`Context.RespData`和`Context.RespStatusCode`中读取到数据的

#### 5.2.2 实现

**a. 修改响应方法**

这里所有写入到响应的地方都要改成写入到`RespData`和`RespStatusCode`这两个字段上:

`context.go`:

```go
// RespJSON 以JSON格式输出相应
func (c *Context) RespJSON(status int, obj any) (err error) {
	data, err := json.Marshal(obj)
	if err != nil {
		return err
	}

	c.Resp.Header().Set("Content-Type", "application/json")
	c.Resp.Header().Set("Content-Length", strconv.Itoa(len(data)))
	c.RespStatusCode = status
	c.RespData = data

	return err
}
```

`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.RespStatusCode = http.StatusNotFound
		ctx.RespData = []byte("Not Found")
		return
	}

	// 命中节点则将路径参数名值对设置到上下文中
	ctx.PathParams = targetNode.pathParams

	// 命中节点则将节点的路由设置到上下文中
	ctx.MatchRoute = targetNode.node.route

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

到目前为止,我们还没有真的将数据刷到响应中,接下来解决这个问题

**b. 写入数据到响应**

这一步的问题在于:该在什么事件节点上把`RespJSON()`和`server()`中提供的响应状态码和响应数据,刷到响应上呢?

答案比较明显:在所有的中间件对响应状态码和响应数据的读写操作(中间件进行写响应就相当于是篡改响应的操作了)都完成后,换言之即响应返回给客户端之前,将把`RespJSON()`和`server()`中提供的响应状态码和响应数据刷到响应上

注意我们中间件的顺序问题:**中间件中在`next(ctx)`之前的代码,是后添加的中间件先执行;中间件中在`next(ctx)`之后的代码,是先添加的中间件先执行**

因此应该**在中间件链的"头部"(这里我们将先添加的中间件称为"尾部",后添加的中间件称为"头部")添加一个在`next(ctx)`之后执行的中间件**:

`httpServer.go`:

```go
// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 执行中间件链
	root := s.serve
	for i := len(s.middlewares) - 1; i >= 0; i-- {
		root = s.middlewares[i](root)
	}

	var m Middleware = func(next HandleFunc) HandleFunc {
		return func(ctx *Context) {
			next(ctx)
			ctx.Resp.Write(ctx.RespData)
			// 若使用者设置了响应码 则刷到响应上
			if ctx.RespStatusCode != 0 {
				ctx.Resp.WriteHeader(ctx.RespStatusCode)
			}
		}
	}
	// 最后注册将响应数据和响应码写入到响应体中的中间件
	// 确保这个中间件是执行完所有对响应码和响应数据的读写操作后才执行的
	// 换言之,确保这个中间件是返回响应之前最后一个执行的
	root = m(root)

	// 查找路由树并执行命中的业务逻辑
	root(ctx)
}
```

**c. 写入失败的错误处理**

还有问题:如果刷响应失败了怎么办?

注意你是不能直接用`log.Fatal()`之类的方法的,因为有可能框架的使用者是不希望直接把日志打印到控制台上的

* step1. 定义日志函数

`httpServer.go`:

```go
// HTTPServer HTTP服务器
type HTTPServer struct {
	router                                   // router 路由树
	middlewares []Middleware                 // middlewares 中间件切片.表示HTTPServer需要按顺序执行的的中间件链
	logFunc     func(msg string, arg ...any) // logFunc 日志函数
}
```

* step2. 处理错误并调用日志函数

`httpServer.go`:

```go
// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 执行中间件链
	root := s.serve
	for i := len(s.middlewares) - 1; i >= 0; i-- {
		root = s.middlewares[i](root)
	}

	var m Middleware = func(next HandleFunc) HandleFunc {
		return func(ctx *Context) {
			next(ctx)

			n, err := ctx.Resp.Write(ctx.RespData)
			if err != nil {
				s.logFunc("响应数据写入失败: %v", err)
			}

			if n != len(ctx.RespData) {
				s.logFunc("响应数据写入不完全, 期望写入: %d 字节, 实际写入: %d 字节", len(ctx.RespData), n)
			}

			// 若使用者设置了响应码 则刷到响应上
			if ctx.RespStatusCode != 0 {
				ctx.Resp.WriteHeader(ctx.RespStatusCode)
			}
		}
	}
	// 最后注册将响应数据和响应码写入到响应体中的中间件
	// 确保这个中间件是执行完所有对响应码和响应数据的读写操作后才执行的
	// 换言之,确保这个中间件是返回响应之前最后一个执行的
	root = m(root)

	// 查找路由树并执行命中的业务逻辑
	root(ctx)
}
```

* step3. 将中间件的处理抽象成一个方法

`httpServer.go`:

```go
// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 执行中间件链
	root := s.serve
	for i := len(s.middlewares) - 1; i >= 0; i-- {
		root = s.middlewares[i](root)
	}

	var m Middleware = func(next HandleFunc) HandleFunc {
		return func(ctx *Context) {
			next(ctx)
			s.flashResp(ctx)
		}
	}
	// 最后注册将响应数据和响应码写入到响应体中的中间件
	// 确保这个中间件是执行完所有对响应码和响应数据的读写操作后才执行的
	// 换言之,确保这个中间件是返回响应之前最后一个执行的
	root = m(root)

	// 查找路由树并执行命中的业务逻辑
	root(ctx)
}

// flashResp 将响应数据和响应码写入到响应体中
func (s *HTTPServer) flashResp(ctx *Context) {
	// 若使用者设置了响应码 则刷到响应上
	if ctx.RespStatusCode != 0 {
		ctx.Resp.WriteHeader(ctx.RespStatusCode)
	}
	
	// 刷响应数据到响应上
	n, err := ctx.Resp.Write(ctx.RespData)
	if err != nil {
		s.logFunc("响应数据写入失败: %v", err)
	}

	if n != len(ctx.RespData) {
		s.logFunc("响应数据写入不完全, 期望写入: %d 字节, 实际写入: %d 字节", len(ctx.RespData), n)
	}
}
```

**注意这里要先刷响应码再刷响应数据**

* step4. 初始化`HTTPServer`时,初始化日志函数

这里如果使用者需要注入自定义的日志函数,仅需传入一个`ServerOption`即可.所以这里要做的只是提供一个默认的日志函数:

`httpServer.go`:

```go
// NewHTTPServer 创建HTTP服务器
// 这里选项的含义其实是指不同的 Option 函数
// 每一个 Option 函数都会对 HTTPServer 实例的不同属性进行设置
func NewHTTPServer(opts ...Option) *HTTPServer {
	server := &HTTPServer{
		router: newRouter(),
		logFunc: func(msg string, arg ...any) {
			fmt.Printf(msg, arg...)
		},
	}

	for _, opt := range opts {
		opt(server)
	}

	return server
}
```

## PART6. 修改测试用例

注意现在就不能再在HandleFunc中直接调用`http.ResponseWriter`的方法了,因为那样会导致中间件拿不到响应码的

`middlewares/open_telemetry/middleware_test.go`:

```go
// Test_MiddlewareBuilder 以创建 web.HandleFunc 的形式,在这个HandleFunc
// 中创建几个具有父子关系的span,并最终在第三方的tracing平台中查看这些span
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件
	tracer := otel.GetTracerProvider().Tracer(instrumentationName)
	builder := &MiddlewareBuilder{
		Tracer: tracer,
	}
	tracingMiddleware := builder.Build()

	// 创建中间件Option
	middlewareOption := web.ServerWithMiddleware(tracingMiddleware)

	// 创建服务器
	s := web.NewHTTPServer(middlewareOption)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		// 创建第1层span
		firstLayerContext1, firstLayerSpan1 := tracer.Start(ctx.Req.Context(), "first_layer_1")
		_, firstLayerSpan2 := tracer.Start(ctx.Req.Context(), "first_layer_2")

		// 创建第2层span 第2层的span是第1层span的子span
		secondLayerContext, secondLayerSpan := tracer.Start(firstLayerContext1, "second_layer")
		// 暂停1s 目的在于: 使得第2层span的开始时间与第1层span的开始时间有较为明显的时间差
		time.Sleep(time.Second)
		// 这里的defer secondLayerSpan.End()其实是可以不写的,因为在第一层span结束的时候,第二层span也会结束

		// 创建第3层的span 第3层的span是第2层span的子span
		_, thirdLayerSpan1 := tracer.Start(secondLayerContext, "third_layer_1")
		// 暂停100ms 先关闭第3层的第1个span
		time.Sleep(100 * time.Millisecond)
		thirdLayerSpan1.End()
		_, thirdLayerSpan2 := tracer.Start(secondLayerContext, "third_layer_2")
		// 暂停300ms 再关闭第3层的第2个span
		// 这样做是为了让第3层的2个span之间是一个有明显时间差的关系
		time.Sleep(300 * time.Millisecond)
		thirdLayerSpan2.End()

		// 再关闭第2层的span
		secondLayerSpan.End()

		// 最后关闭第1层的span
		firstLayerSpan1.End()
		firstLayerSpan2.End()

		ctx.RespJSON(http.StatusOK, User{Name: "test"})
	}

	// 设置TracerProvider
	initZipkin(t)

	// 注册路由并启动服务器
	s.GET("/user", handleFunc)
	s.Start(":8092")
}

type User struct {
	Name string `json:"name"`
}
```

![拿到了状态码但是没拿到host](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-6f6714f05c41fc77bc924bfb21d2c700dc1ef34a%2F%E6%8B%BF%E5%88%B0%E4%BA%86%E7%8A%B6%E6%80%81%E7%A0%81%E4%BD%86%E6%98%AF%E6%B2%A1%E6%8B%BF%E5%88%B0host.png?alt=media)

最后一个bug:没拿到Host

这里是因为取Host时字段写错了,一个比较好修的Bug:

`middlewares/open_telemetry/middlewareBuilder.go`:

```go
package open_telemetry

import (
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/propagation"
	"go.opentelemetry.io/otel/trace"
	"web"
)

// instrumentationName 仪表盘名称 通常以包名作为仪表盘名称
// TODO: 如果真的把这个框架 作为一个独立的库发布 这里要改成github.com/xxx/xxx这样的形式
const instrumentationName = "web/middlewares/open_telemetry"

// MiddlewareBuilder openTelemetry中间件构建器
type MiddlewareBuilder struct {
	Tracer trace.Tracer // Tracer 追踪器
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		if m.Tracer == nil {
			m.Tracer = otel.GetTracerProvider().Tracer(instrumentationName)
		}

		return func(ctx *web.Context) {
			reqCtx := ctx.Req.Context()
			carrier := propagation.HeaderCarrier(ctx.Req.Header)
			reqCtx = otel.GetTextMapPropagator().Extract(reqCtx, carrier)

			reqCtx, span := m.Tracer.Start(reqCtx, "unknown")
			defer span.End()

			attributes := []attribute.KeyValue{
				attribute.String("http.method", ctx.Req.Method),
				attribute.String("http.url", ctx.Req.URL.String()),
				attribute.String("http.scheme", ctx.Req.URL.Scheme),
				attribute.String("http.host", ctx.Req.Host),
			}
			span.SetAttributes(attributes...)

			ctx.Req = ctx.Req.WithContext(reqCtx)

			next(ctx)

			if ctx.MatchRoute != "" {
				span.SetName(ctx.MatchRoute)
			}

			// 强制类型转换(假定这里我们有一个自己实现的MyResponseWriter)
			// 问题在于:你根本不知道ctx.Resp是什么类型的 可能是http包内的私有类型
			// 还有可能是使用者通过中间件提供的自定义类型
			// myResp, ok := ctx.Resp.(MyResponseWriter)

			// 4. 请求完成后记录响应码
			span.SetAttributes(attribute.Int("http.status_code", ctx.RespStatusCode))
		}
	}
}
```

![获取到host和响应码的span](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-86d4ba80301f46d2b9f380a26b995ec863fd365b%2F%E8%8E%B7%E5%8F%96%E5%88%B0host%E5%92%8C%E5%93%8D%E5%BA%94%E7%A0%81%E7%9A%84span.png?alt=media)

至此,链路追踪中间件开发完成

## 附录

TODO: 我在写的时候,想过将`http.ResponseWriter`直接设置为私有字段,有时间了我自己试一下


# 7.04 Middleware-OpenTelemetry总结

## PART1. 关联上游

![关联上游](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d42dbe82bc684bb4c55197d315d5c6f975aa3abc%2F%E5%85%B3%E8%81%94%E4%B8%8A%E6%B8%B8.png?alt=media)

记录span的Context要关联上请求中的span(`Context.Req.Context()`).

**如果你不知道该在span中记录什么标签,那你就去抄Kratos或go-zero或ego中的**

## PART2. 获取响应状态码

![获取响应状态码](https://github.com/step-by-step-wiki/GoBook/blob/main/img/16.Middleware-OpenTelemetry总结/获取响应状态码.png)

这里额外提一点,我们在span中记录的是请求命中的路由,而非请求的路径.这是因为路径中可能携带有多个路径参数,使得整个URL变得较为复杂

另外这里是因为我们无法从`http.ResponseWriter`直接获取响应状态码,只能是通过**将响应数据和响应码分别写入到`Context`的两个成员属性上,在执行完所有对响应码和响应数据的读写操作后(即返回相应之前),将响应数据和响应码刷到响应中**的方式,来获取响应数据和响应码.**相当于缓存了响应数据和响应码.等到所有的中间件都操作完了这份缓存的响应数据和响应码之后,返回响应之前,将它们刷到响应中**

![flashResp的执行时机](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-cfcf16a60f04c7e9c03d0095ac8166474700a6bf%2FflashResp%E7%9A%84%E6%89%A7%E8%A1%8C%E6%97%B6%E6%9C%BA.png?alt=media)

TODO:可能我在AOP那一章节里对于中间件的组装顺序和执行顺序的解释是有误的,这个我需要确认


# 7.05 Prometheus详解

可观测性的3个关键方面:

* 日志
* 追踪
* 指标

现在就只差指标(Metrics)没有实现了

Metrics虽然很多公司内部有自研的框架,但是在开源里面,Prometheus应该算是用户最多的框架.

OpenTelemetry适配了Prometheus,但是我们可以额外提供一个Prometheus的Middleware,看看Prometheus的效果

![dashboard示例](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-5735cee17a75f3d24fb8b90044172279b400f2ee%2Fdashboard%E7%A4%BA%E4%BE%8B.png?alt=media)

## PART1. Metrics的类型

### 1.1 Counter

Counter:计数器,统计次数,比如说某件事发生了多少次.**Counter类型是只增不减的**,例如统计我的程序**历史总共发生panic的次数**,这种次数只会增加而不会减少(你不可能看到一你自己写的程序发生panic的共计次数越来越少)

![Counter示例](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-24e3482d9632c0cba4cf369d56ce675c838adaf8%2FCounter%E7%A4%BA%E4%BE%8B.png?alt=media)

### 1.2 Gauge

Gauge:度量,它可以增加也可以减少,比如说当前正在处理的请求数.**Gauge类型可增可减**,例如统计我的程序**当前正在处理的请求数**,这种次数随着一个请求进来就加1,随着一个请求被处理完就减1

![Gauge示例](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-ab7b68ad4dda00a15205244e45daa86dc5889205%2FGauge%E7%A4%BA%E4%BE%8B.png?alt=media)

### 1.3 Histogram

Histogram:柱状图,对观察对象进行采样,然后分到一个个桶里面

![Histogram示例](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-1f6ddf68b87289dac60c65ca532294b54e11cb13%2FHistogram%E7%A4%BA%E4%BE%8B.png?alt=media)

### 1.4 Summary

Summary:采样点按照百分位进行统计,比如99线、999线等

99线:99%的请求响应时间都在这条线之内.例如划定99线为1秒,则99线要求99%的请求需要在1s内能够返回

![Summary示例](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-294e2c2500efacfbf58265f619a4675f8eb87d84%2FSummary%E7%A4%BA%E4%BE%8B.png?alt=media)

图中蓝框部分的`0.99`即为99线;`0.95`即为95线;注意`0.5`不是平均值,而是中位数;`Average`是平均线

### 1.5 Histogram和Summary的区别

在Prometheus监控系统中,Histogram和Summary都是用于跟踪事件发生的频率和大小的度量类型,但它们在功能和用法上有一些关键区别:

1. **Histogram**:
   * **分布桶(Buckets)**:Histogram通过定义一系列的桶(buckets)来测量事件的分布.每个桶覆盖了一个值的范围,并且会计数落入该范围内的事件
   * **适用性**:Histogram特别适合于**具有固定范围和需要聚合**的场景.例如请求延迟或响应大小
   * **聚合友好**:Histogram的聚合更加简单,因为它可以跨多个实例合并.这使得Histogram非常适合在高维度环境中使用,如分布式系统
   * **配置**:需要预先定义桶的范围,这可能需要根据具体应用进行调整
2. **Summary**:
   * **分位数(Quantiles)**:Summary类型计算并提供样本的分位数(quantiles),例如p50、p90、p99等。这允许你直接从度量中获取特定分位数的值。
   * **适用性**:Summary适用于需要准确分位数的场景,但通常在单个服务或应用级别使用
   * **聚合限制**:Summary的一个重要限制是**它不容易跨多个实例或作业进行聚合,因为分位数的计算通常是本地的**
   * **性能考量**:计算分位数比Histogram更为复杂和资源密集,特别是在高流量下

### 1.6 Vector

在Prometheus中,"Vector"类型是一种**数据格式(注意只是一种数据格式,或者称之为数据结构,它并不是Prometheus中的数据类型)**,它代表了一组时间序列数据.Vector类型主要分为两种:即时向量(Instant Vector)和区间向量(Range Vector).它们用于采集和表示不同类型的指标:

1. **即时向量(Instant Vector)**:
   * 这代表了在某一特定时刻的一系列时间序列数据
   * 即时向量通常用于表示瞬时的数据点,比如当前的CPU使用率、内存使用量、正在运行的进程数等
   * 在Prometheus查询中,当你查询一个特定的时间点或最新数据时,结果通常是一个即时向量
2. **区间向量(Range Vector)**:
   * 区间向量包含了一段时间内的时间序列数据
   * 它们主要用于需要观察一段时间内的趋势或模式的指标,例如过去1小时的平均负载、过去24小时内的最大网络流量等
   * 在Prometheus查询中,区间向量经常用于计算平均值、求和、最大值或最小值等,这需要对一段时间内的数据进行操作

Vector类型在Prometheus中非常核心,因为它们能够有效地表示和处理时间序列数据,这是Prometheus监控系统的基础.无论是即时数据还是一段时间内的数据,Vector类型都能够提供灵活和强大的方式来查询和分析指标

## PART2. Prometheus基本用法

### 2.1 Counter

![Counter基本用法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-a211833a78c426ad6ec04e0b37a6454dd2244df0%2FCounter%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95.png?alt=media)

* Namespace:APP名
* Subsystem:
  * 横向分割命名:按各个中间件划分来命名,例如HTTP、Grpc、Kafka
  * 纵向分割命名:按各个微服务划分来命名,例如user、order
  * 具体采用哪种方式,取决于你在什么地方做指标采集.如果像是在本节课中要实现的,在middleware层做指标采集,那么可能横向分割命名更合适;如果是在具体的某个服务中做指标采集,那么可能纵向分割命名更合适
* Name: 指标名称

**注意:截图中的代码有误.`Namespace`、`Subsystem`和`Name`的名称中均不能使用`-`,使用`_`代替即可.这是一个限制**

`prometheus.MustRegister()`:用于注册指标.该方法如果注册失败不会返回`error`,而是直接`panic`.注意:**对于同一个指标,如果调用`prometheus.MustRegister()`方法进行注册的话,则只能调用1次.否则会触发`panic`**

`prometheus.Register()`:用于注册指标.该方法如果注册失败会返回`error`

### 2.2 Gauge

![Gauge基本用法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d81f0f63cd4a29c2a6d7fac9224984f2f2d1425d%2FGauge%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95.png?alt=media)

### 2.3 Histogram

**分桶不是为了在采集指标的数值上均匀,而是为了让被采集的指标在分布上均匀**.

假定图中是针对响应时长进行分桶,那么这里的分桶就是:

* (0, 10]
* (10, 50]
* (50, 100]
* (100, 200]
* (200, 500]
* (500, 1000]
* (1000, 10000]

以上单位皆为`ms`.举个例子,可能一个请求命中了缓存,不到10ms就可以响应;没有命中缓存,走正常的业务逻辑去查库,那么大部分请求的响应时长在10-200ms之间.可以想象,直方图会呈近似正态分布的形状.500ms以上的区间就可以将粒度设置的粗一些,这样你可以在正态分布的右侧看到较多区间,且形状更接近正态分布

![分布较好与分布不好的Histogram](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-4d0c2032c0eaafc4184e2b0f9465d3c870b6825c%2F%E5%88%86%E5%B8%83%E8%BE%83%E5%A5%BD%E4%B8%8E%E5%88%86%E5%B8%83%E4%B8%8D%E5%A5%BD%E7%9A%84Histogram.png?alt=media)

### 2.4 Summary

![Summary基本用法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3cb30fa18823b404ad6caf5c63270604392c937a%2FSummary%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95.png?alt=media)

误差越小需要占用越多的CPU资源.注意**Summary是客户端计算好推给服务端的,而Histogram是客户端直接推给服务端,然后由服务端完成计算的**.因此你的误差越小,对客户端的系统性能影响就越大.

那么为什么会有误差呢?这里我们还是以请求响应时长为例:如果你有100个请求,统计99%的请求的响应时间,那么你可以统计99个请求的响应时长;那么如果你有101个请求,你是没法统计99.01个请求的响应时长的,因此有一个误差区间

### 2.5 Vector

![Vector基本用法](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-12d4acaf5514c3e5221fa65978c810a6d29addb7%2FVector%E5%9F%BA%E6%9C%AC%E7%94%A8%E6%B3%95.png?alt=media)

* ConstLabels:固定的标签.即指标中的每一份数据都会有这些标签
* Labels:动态设定的标签

那么为什么要动态设定标签呢? 直接把标签在ConstLabels中全都预定义好不就可以了吗?

按图中的例子,如果为`/user/:id`、`POST`、`200`预定义一组标签,那么`/user/:id`、`POST`、`201`是不是还要预定义一组标签?

注意`WithLabelValues()`返回的是一个Summary,而非是Vector.再次注意,Summary是一种数据类型,而Vector只是一种数据结构,这里只是Vector中包含了多个Summary,类似于一个\[]int中包含了多个int一样.

`WithLabelValues("/user/:id", "POST", "200")`:如果没有标签和给定值相同的Summary则创建,如果有则返回该Summary.

`Observe(128)`:写入Summary统计的指标数据


# 7.06 Middleware-Prometheus

本节课工程结构如下:

```
(base) yanglei@yuanhong 04-prometheus % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│   ├── access_log
│   │   ├── accessLog.go
│   │   ├── accessLog_test.go
│   │   └── middlewareBuilder.go
│   └── open_telemetry
│       ├── docker-compose.yaml
│       ├── middlewareBuilder.go
│       └── middleware_test.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

3 directories, 23 files
```

## PART1. middlewareBuilder

按照惯例,和之前写`access_log`和`open_telemetry`一样,写prometheus的中间件也是从`middlewareBuilder`开始的

`middlewares/prometheus/middlewareBuilder.go`:

```go
package prometheus

import "web"

// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

### 1.1 创建vector

`middlewares/prometheus/middlewareBuilder.go`:

```go
package prometheus

import (
	"github.com/prometheus/client_golang/prometheus"
	"web"
)

// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
	Namespace string // Namespace APP名称
	Subsystem string // Subsystem 子系统/模块名称
	Name      string // Name 指标名称
	Help      string // Help 指标的描述信息
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	labels := []string{
		"pattern", // pattern 命中的路由
		"method",  // method 请求方法
		"status",  // status 响应状态码
	}

	vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
		Namespace: m.Namespace,
		Subsystem: m.Subsystem,
		Name:      m.Name,
		Help:      m.Help,
	}, labels)
	
	// 注册指标
	prometheus.MustRegister(vector)
	
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

这里不要忘记注册指标

### 1.2 记录指标

很明显,我们要上报指标,首先得有指标才能上报.想要有指标,那么一定是在HandleFunc执行完毕之后,才能拿到指标的数据(例如响应时长).

`middlewares/prometheus/middlewareBuilder.go`:

```go
package prometheus

import (
	"github.com/prometheus/client_golang/prometheus"
	"strconv"
	"time"
	"web"
)

// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
	Namespace string // Namespace APP名称
	Subsystem string // Subsystem 子系统/模块名称
	Name      string // Name 指标名称
	Help      string // Help 指标的描述信息
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	labels := []string{
		"pattern", // pattern 命中的路由
		"method",  // method 请求方法
		"status",  // status 响应状态码
	}

	vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
		Namespace: m.Namespace,
		Subsystem: m.Subsystem,
		Name:      m.Name,
		Help:      m.Help,
	}, labels)
	
	// 注册指标
	prometheus.MustRegister(vector)

	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			startTime := time.Now()

			// 为防止后续的中间件和HandleFunc中发生panic导致指标不记录
			// 故在此处将指标记录的代码放在defer中
			defer func() {
				// 响应时长
				duration := time.Since(startTime).Milliseconds()
				
				vector.WithLabelValues(
					// 命中的路由
					ctx.MatchRoute,
					// 请求方法
					ctx.Req.Method,
					// 响应状态码
					strconv.Itoa(ctx.RespStatusCode),
				).Observe(float64(duration))
			}()

			next(ctx)
		}
	}
}
```

这里由于需要等到HandleFunc执行完毕,响应从中间件链上回来的时候,才能拿到各种指标(响应时长、命中的路由、请求方法、响应状态码),因此为了防止在中间件链上发生panic导致无法记录指标,因此我们将记录指标的代码放在`defer`中执行

注意:这里`defer`中的代码也可以放在一个goroutine中以异步的方式执行,这样就可以不阻塞主线程了.可能有人会问:那如果中间件责任链上发生了panic或者突然宕机了,那么异步采集的指标不就丢失了吗?

其实这是一个伪命题.**因为你正常的业务都panic了或者突然宕机,你不可能把关注点放在一个指标是否采集成功这件事上,正常人想的都是突然宕机或者发生了意料之外的panic,我的业务是否还安好**.

TODO:试一下异步的goroutine方案

### 1.3 设置百分比和误差

`middlewares/prometheus/middlewareBuilder.go`:

```go
package prometheus

import (
	"github.com/prometheus/client_golang/prometheus"
	"strconv"
	"time"
	"web"
)

// MiddlewareBuilder prometheus中间件构建器
type MiddlewareBuilder struct {
	Namespace string // Namespace APP名称
	Subsystem string // Subsystem 子系统/模块名称
	Name      string // Name 指标名称
	Help      string // Help 指标的描述信息
}

// Build 构建中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	labels := []string{
		"pattern", // pattern 命中的路由
		"method",  // method 请求方法
		"status",  // status 响应状态码
	}

	vector := prometheus.NewSummaryVec(prometheus.SummaryOpts{
		Namespace: m.Namespace,
		Subsystem: m.Subsystem,
		Name:      m.Name,
		Help:      m.Help,
		// 采样率 即百分比和误差
		Objectives: map[float64]float64{
			0.5:  0.01,
			0.75: 0.01,
			0.90: 0.005,
			// 99线
			0.99: 0.001,
			// 999线
			0.999: 0.0001,
		},
	}, labels)
	
	// 注册指标
	prometheus.MustRegister(vector)

	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			startTime := time.Now()

			// 为防止后续的中间件和HandleFunc中发生panic导致指标不记录
			// 故在此处将指标记录的代码放在defer中
			defer func() {
				// 响应时长
				duration := time.Since(startTime).Milliseconds()

				vector.WithLabelValues(
					// 命中的路由
					ctx.MatchRoute,
					// 请求方法
					ctx.Req.Method,
					// 响应状态码
					strconv.Itoa(ctx.RespStatusCode),
				).Observe(float64(duration))
			}()

			next(ctx)
		}
	}
}
```

关于误差的描述可以[参见此处](https://github.com/rayallen20/GoInAction/blob/master/note/%E7%AC%AC2%E5%91%A8-Web%E6%A1%86%E6%9E%B6%E4%B9%8BContext%E4%B8%8EAOP%E6%96%B9%E6%A1%88/PART3.%20Middleware/17.%20Prometheus%E8%AF%A6%E8%A7%A3.md#24-summary)

## PART2. 编写测试用例

### 2.1 创建Server和Middleware

`middlewares/prometheus/middleware_test.go`:

```go
package prometheus

import (
	"math/rand"
	"net/http"
	"testing"
	"time"
	"web"
)

// Test_MiddlewareBuilder 测试MiddlewareBuilder
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件构建器
	builder := &MiddlewareBuilder{
		// Tips: 此处的Namespace、Subsystem、Name、Help的值均不可以出现-
		Namespace: "my_framework",
		Subsystem: "web",
		Name:      "http_response",
		Help:      "metric_help",
	}

	// 创建中间件Option
	options := web.ServerWithMiddleware(builder.Build())

	// 创建服务器
	server := web.NewHTTPServer(options)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		// 随机sleep 1-1001ms 模拟请求时长
		randomValue := rand.Intn(1000) + 1
		time.Sleep(time.Duration(randomValue) * time.Millisecond)

		ctx.RespJSON(http.StatusAccepted, &User{Name: "Tom"})
	}

	// 注册路由并启动服务器
	server.GET("/user", handleFunc)
	server.Start(":8080")
}

type User struct {
	Name string
}
```

这部分代码都比较常规,之前都写过很多遍了,不解释.

### 2.2 暴露指标

`middlewares/prometheus/middleware_test.go`:

```go
package prometheus

import (
	"github.com/prometheus/client_golang/prometheus/promhttp"
	"math/rand"
	"net/http"
	"testing"
	"time"
	"web"
)

// Test_MiddlewareBuilder 测试MiddlewareBuilder
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件构建器
	builder := &MiddlewareBuilder{
		// Tips: 此处的Namespace、Subsystem、Name、Help的值均不可以出现-
		Namespace: "my_framework",
		Subsystem: "web",
		Name:      "http_response",
		Help:      "metric_help",
	}

	// 创建中间件Option
	options := web.ServerWithMiddleware(builder.Build())

	// 创建服务器
	server := web.NewHTTPServer(options)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		// 随机sleep 1-1001ms 模拟请求时长
		randomValue := rand.Intn(1000) + 1
		time.Sleep(time.Duration(randomValue) * time.Millisecond)

		ctx.RespJSON(http.StatusAccepted, &User{Name: "Tom"})
	}

	// 监听另外一个端口 该端口用于暴露指标让prometheus采集
	go func() {
		http.Handle("/metrics", promhttp.Handler())
		// 通常用于采集指标的端口不会和业务端口混合在一起
		// 因为业务端口通常是需要暴露到外网的 但是指标采集端口通常是不需要的
		http.ListenAndServe(":8082", nil)
	}()

	// 注册路由并启动服务器
	server.GET("/user", handleFunc)
	server.Start(":8080")
}

type User struct {
	Name string
}
```

注意:通常暴露指标的端口和业务端口不会混用的.因为业务端口要暴露到外网,而暴露指标的端口大概率是不需要暴露到外网的

### 2.3 查看结果

![请求路由](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-07506429065a4baf83f5d50631d162067e78f296%2F%E8%AF%B7%E6%B1%82%E8%B7%AF%E7%94%B1.png?alt=media)

![暴露指标](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-3fb44c11b73c97858618d4ee024090e9e03faaa3%2F%E6%9A%B4%E9%9C%B2%E6%8C%87%E6%A0%87.png?alt=media)


# 7.07 Middleware-错误页面

本节课工程结构如下:

```
(base) yanglei@yuanhong 05-errPage % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│   ├── access_log
│   │   ├── accessLog.go
│   │   ├── accessLog_test.go
│   │   └── middlewareBuilder.go
│   ├── open_telemetry
│   │   ├── docker-compose.yaml
│   │   ├── middlewareBuilder.go
│   │   └── middleware_test.go
│   └── prometheus
│       ├── middlewareBuilder.go
│       └── middleware_test.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

4 directories, 25 files
```

## PART1. 需求概述

我们通常有一个需求:如果一个响应返回了404,那么应该显示一个错误页面,停留在这个错误页面几秒钟后,重定向到一个默认页面,比如说重定向到首页

但有一个棘手的问题在于:**不是所有的404都是要重定向的**.比如说你是异步加载数据的RESTful请求,在打开页面之后异步加载用户详情,即便 404了也不应该重定向.因为请求API,404了就是404了,不需要你重定向到某个页面上.你的客户端也不会用你的404页面

目前我们没有设计可路由的中间件,因此没有办法给使用者提供一个入口,该入口用于控制某个路由是否需要重定向到某个页面.

此处我们先实现一个比较粗暴的设计:**当检测到响应码为某个特定值时,篡改响应为某个错误页面**

## PART2. 实现

### 2.1 基本构型

这个都写了N遍了,没什么要解释的

`middleware/err_page/middlewareBuilder.go`:

```go
package err_page

import "web"

// MiddlewareBuilder 错误页面中间件构造器
type MiddlewareBuilder struct {
}

// Build 构造错误页面中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

### 2.2 检测响应码并渲染对应的错误页面

那首先肯定是需要一个map用于记录响应码与其对应错误页面的映射关系:

`middleware/err_page/middlewareBuilder.go`:

```go
package err_page

import "web"

// MiddlewareBuilder 错误页面中间件构造器
type MiddlewareBuilder struct {
	respPages map[int][]byte // respPages 用于存储响应码与其对应的错误页面 其中key为响应码 value为错误页面的内容
}

// Build 构造错误页面中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

接下来的操作就比较简单了,判断响应码是否为需要渲染错误页面的响应码,如果是,则渲染对应的错误页面即可:

`middleware/err_page/middlewareBuilder.go`:

```go
package err_page

import "web"

// MiddlewareBuilder 错误页面中间件构造器
type MiddlewareBuilder struct {
	respPages map[int][]byte // respPages 用于存储响应码与其对应的错误页面 其中key为响应码 value为错误页面的内容
}

// Build 构造错误页面中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)

			// 判断响应码是否为需要篡改响应的响应码 如果是则篡改响应
			respPage, ok := m.respPages[ctx.RespStatusCode]
			if ok {
				ctx.RespData = respPage
			}
		}
	}
}
```

其实到这里主流程就实现完毕了已经.但还有一些边缘问题,例如:

* 缺少一个用于让框架的使用者注册自己的响应码和错误页面的入口
* 缺少初始化`MiddlewareBuilder`的过程
  * 这里因为`MiddlewareBuilder. respPages`是一个需要初始化的数据类型,因此

### 2.3 注册响应码与错误页面

这2个缺陷实现起来也很简单

`middleware/err_page/middlewareBuilder.go`:

```go
package err_page

import "web"

// MiddlewareBuilder 错误页面中间件构造器
type MiddlewareBuilder struct {
	respPages map[int][]byte // respPages 用于存储响应码与其对应的错误页面 其中key为响应码 value为错误页面的内容
}

// Build 构造错误页面中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)

			// 判断响应码是否为需要篡改响应的响应码 如果是则篡改响应
			respPage, ok := m.respPages[ctx.RespStatusCode]
			if ok {
				ctx.RespData = respPage
			}
		}
	}
}

// AddCode 添加响应码与其对应的错误页面
func (m *MiddlewareBuilder) AddCode(status int, page []byte) *MiddlewareBuilder {
	if m.respPages == nil {
		m.respPages = make(map[int][]byte)
	}
	m.respPages[status] = page
	
	// Tips: 此处返回 *MiddlewareBuilder 是为了支持链式调用
	return m
}

// NewMiddlewareBuilder 初始化错误页面中间件构造器
func NewMiddlewareBuilder() *MiddlewareBuilder {
	return &MiddlewareBuilder{
		respPages: make(map[int][]byte),
	}
}
```

## PART3. 测试

`middleware/err_page/middleware_test.go`:

```go
package err_page

import (
	"testing"
	"web"
)

// Test_MiddlewareBuilder 测试错误页面中间件构造器
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建中间件构建器
	builder := NewMiddlewareBuilder()
	builder.
		AddCode(404, []byte(`
<html>
	<head>
		<title>404 Not Found</title>
	</head>

	<body>
		<h1>404 Not Found</h1>
	</body>
</html>
`)).
		AddCode(500, []byte(`
<html>
	<head>
		<title>500 Internal Server Error</title>
	</head>

	<body>
		<h1>500 Internal Server Error</h1>
	</body>
</html>
`))

	// 创建中间件Option
	options := web.ServerWithMiddleware(builder.Build())

	// 创建服务器
	server := web.NewHTTPServer(options)

	// 启动服务器
	server.Start(":8080")
}
```

这个测试用例中,我们都不需要注册路由,只需要随便访问一个路由,看是否能渲染我们定义的错误页面即可:

![篡改的404页面](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-4a7a11ab1882e2b80a3a07f45af2d90cfc4a1e8f%2F%E7%AF%A1%E6%94%B9%E7%9A%84404%E9%A1%B5%E9%9D%A2.png?alt=media)

TODO:做完作业考虑如何实现针对指定路由或指定方法的错误页面中间件


# 7.08 Middleware-从panic中恢复

本节课工程结构如下:

```
(base) yanglei@yuanhong 06-recoverPanic % tree ./
./
├── context.go
├── context_test.go
├── go.mod
├── go.sum
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
├── matchNode.go
├── middleware.go
├── middleware_test.go
├── middlewares
│   ├── access_log
│   │   ├── accessLog.go
│   │   ├── accessLog_test.go
│   │   └── middlewareBuilder.go
│   ├── err_page
│   │   ├── middlewareBuilder.go
│   │   └── middleware_test.go
│   ├── open_telemetry
│   │   ├── docker-compose.yaml
│   │   ├── middlewareBuilder.go
│   │   └── middleware_test.go
│   └── prometheus
│       ├── middlewareBuilder.go
│       └── middleware_test.go
├── node.go
├── option.go
├── router.go
├── router_test.go
├── safeContext.go
├── serverInterface.go
└── stringValue.go

5 directories, 27 files
```

## PART1. 需求概述

框架使用者在他们自己的代码中如果不小心触发了一些panic,那么前端会收到一个错误,且同时整个进程也就结束了.正常情况下,即使某个请求触发了panic,也不应该让整个进程都挂了,因为还有其他不会触发panic的路由是可用的.因此我们需要写一个捕获panic的中间件并返回一个固定的错误,确保即使框架使用者触发了panic,整个进程不会宕掉,

## PART2. 实现

### 2.1 基本构型

`middleware/recover_panic/middlewareBuilder.go`:

```go
package recover_panic

import "web"

// MiddlewareBuilder 捕获panic中间件的构建器
type MiddlewareBuilder struct {
	
}

// Build 构建捕获panic中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
		}
	}
}
```

### 2.2 捕获panic并篡改响应

`middleware/recover_panic/middlewareBuilder.go`:

```go
package recover_panic

import "web"

// MiddlewareBuilder 捕获panic中间件的构建器
type MiddlewareBuilder struct {
	StatusCode int    // StatusCode 捕获panic时的响应状态码
	Data       []byte // Data 捕获panic时的响应数据
}

// Build 构建捕获panic中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 捕获panic并篡改响应
			defer func() {
				// Tips: 这里的err的类型是ary，而不是error
				if err := recover(); err != nil {
					ctx.RespStatusCode = m.StatusCode
					ctx.RespData = m.Data
				}
			}()
			
			next(ctx)
		}
	}
}
```

### 2.3 捕获panic时记录日志

`middleware/recover_panic/middlewareBuilder.go`:

```go
package recover_panic

import "web"

// MiddlewareBuilder 捕获panic中间件的构建器
type MiddlewareBuilder struct {
	StatusCode int                    // StatusCode 捕获panic时的响应状态码
	Data       []byte                 // Data 捕获panic时的响应数据
	LogFunc    func(ctx *web.Context) // LogFunc 捕获panic时的日志记录函数 (记录整个ctx)
	// LogFunc    func(err any)          // LogFunc 捕获panic时的日志记录函数 (记录panic的内容)
	// LogFunc    func(stack string)     // LogFunc 捕获panic时的日志记录函数 (记录调用栈)
}

// Build 构建捕获panic中间件
func (m *MiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 捕获panic 篡改响应 并记录日志
			defer func() {
				// Tips: 这里的err的类型是ary，而不是error
				if err := recover(); err != nil {
					ctx.RespStatusCode = m.StatusCode
					ctx.RespData = m.Data

					// 记录日志
					if m.LogFunc != nil {
						m.LogFunc(ctx)
					}
				}
			}()
			
			next(ctx)
		}
	}
}
```

这里的日志记录函数,不一定非要记录Context,也可以记录panic的内容,或者记录发生panic时的堆栈信息

## PART2. 测试

`middleware/recover_panic/middleware_test.go`:

```go
package recover_panic

import (
	"fmt"
	"testing"
	"web"
)

// Test_MiddlewareBuilder 测试捕获panic中间件构造器
func Test_MiddlewareBuilder(t *testing.T) {
	// 创建捕获panic中间件构造器
	builder := &MiddlewareBuilder{
		StatusCode: 500,
		Data:       []byte("panic error"),
		LogFunc: func(ctx *web.Context) {
			fmt.Printf("panic路径: %s\n", ctx.Req.URL.Path)
		},
	}

	// 构建捕获panic中间件
	option := web.ServerWithMiddleware(builder.Build())

	// 创建web服务器
	server := web.NewHTTPServer(option)

	// 创建HandleFunc
	handleFunc := func(ctx *web.Context) {
		panic("test panic")
	}

	// 注册路由并启动服务器
	server.GET("/user", handleFunc)
	server.Start(":8080")
}
```

运行结果:

![panic页面](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-fbf0704d6418c7682d2a295e5a81b318f4c7b013%2Fpanic%E9%A1%B5%E9%9D%A2.png?alt=media)

```
=== RUN   Test_MiddlewareBuilder
panic路径: /user
```

Beego就是一种侵入式的设计,Beego中的[`RecoverPanic`](https://github.com/beego/beego/blob/develop/server/web/config.go#L65)和[`RecoverFunc`](https://github.com/beego/beego/blob/develop/server/web/config.go#L106)实际上都是和核心逻辑耦合在一起的,并不是一种良构的设计.

而我们的设计,是否使用捕获panic的逻辑与核心逻辑就完全无关了.而且完全没有侵入任何核心逻辑.还是那句话,**无侵入式的设计,才是高明的设计**.


# 7.09 Middleware总结和面试

## PART1. Filter要不要考虑时机的问题?

例如:**要不要设计类似Beego那种复杂的在不同阶段运行的Filter(就是Middleware)?**

这个问题和我们之前讨论的其它问题都不太一样,是因为用户完全没办法自己支持,只能依赖于框架支持(也就是必须侵入式地修改框架),类似[Beego中定义的各种执行点](https://github.com/beego/beego/blob/master/server/web/router.go#L38)

如果我们的框架要支持这种设计,就需要在`HTTPServer.serve()`方法中根据不同的flag做各种拦截,类似[Beego中根据flag的值做拦截](https://github.com/beego/beego/blob/master/server/web/router.go#L829),形如:

```go
// serve 查找路由树并执行命中的业务逻辑
func (s *HTTPServer) serve(ctx *Context) {
	// 检测并执行 beforeRoute 的filter

	method := ctx.Req.Method
	path := ctx.Req.URL.Path
	targetNode, ok := s.findRoute(method, path)

	// 检测并执行 afterRoute 的filter

	// 没有在路由树中找到对应的路由节点 或 找到了路由节点的处理函数为空(即NPE:none pointer exception 的问题)
	// 则返回404
	if !ok || targetNode.node.HandleFunc == nil {
		ctx.RespStatusCode = http.StatusNotFound
		ctx.RespData = []byte("Not Found")
		return
	}

	// 命中节点则将路径参数名值对设置到上下文中
	ctx.PathParams = targetNode.pathParams

	// 命中节点则将节点的路由设置到上下文中
	ctx.MatchRoute = targetNode.node.route

	// 检测并执行 beforeExecute 的filter

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

	// 检测并执行 afterExecute 的filter
}
```

这里我们没有执行beforeStatic的地方,因为我们后续采用其他的方案支持.

也就是说,如果框架的使用者真的需要这种在不同阶段执行的Filter,那么就**必须侵入式地修改框架的代码,或者我们在实现`HTTPServer.serve()`方法时事先把这个空缺留好**.

那么问题来了,要不要事先把这个空缺留好?

理论上来说是需要考虑的,但是我们可以推迟到用户真正需要的时候再来评估

**因为大多数场景都是不需要考虑这个问题的,已有的设计完全能够满足**

![Middleware的时机](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-b1aa1c03776dea41d52d3a2455ac2fff285a1bc0%2FMiddleware%E7%9A%84%E6%97%B6%E6%9C%BA.png?alt=media)

## PART2. Middleware要不要考虑顺序问题?

理论上来说,**每一个Middleware都应该不依赖于其它的Middleware**.

但这只是一个美好的希望,比如在我们已经实现的几个Middleware中,处理Panic的Middleware很显然应该在最外层,也就是紧接着`HTTPServer.flashResp()`的位置上,错误处理应该在可观测性之后.但现状并不是这样的:

![Middware的执行顺序-现状](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-33e250525bfa372e685e02dd7fe4ad518f0266c0%2FMiddware%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F-%E7%8E%B0%E7%8A%B6.jpg?alt=media)

TODO: 如果flashResp后边又跟一个Panic呢?

有些设计者可能会认为错误处理应该在可观测性之前,因为他们只关心业务逻辑的执行情况,并不关心框架的错误处理所消耗的时长,如下图示:

![Middware的执行顺序-错误处理在前](https://github.com/step-by-step-wiki/GoBook/blob/main/img/21.Middleware总结和面试/Middware的执行顺序-错误处理在前.jpg)

这样的顺序也是没问题的.Middleware的执行顺序没有严格的要求,但是**如果前边的Middleware和后边的Middleware是有关联的(例如恢复Panic的Middleware要恢复后边所有Middleware中发生的panic),那么对顺序就有要求了**.当然你要把恢复Panic的Middleware放在最后边也不是不可以,那就意味着你作为框架的设计者,只恢复HandleFunc产生的panic.

又比如,从业务上来看,鉴权应该在很靠前的位置,限流可以在鉴权前面,也可以在鉴权后面,取决于业务

![Middware的执行顺序-鉴权中间件的顺序](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-c27801bf7c975323b17d6f779e74b40f60ea8021%2FMiddware%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F-%E9%89%B4%E6%9D%83%E4%B8%AD%E9%97%B4%E4%BB%B6%E7%9A%84%E9%A1%BA%E5%BA%8F.jpg?alt=media)

假定此时还有限流中间件,那么限流与鉴权的顺序关系如下:

![Middware的执行顺序-鉴权与限流](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-43ef322f6d3fb3b3e84c638d0c3b3dd301c5e732%2FMiddware%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F-%E9%89%B4%E6%9D%83%E4%B8%8E%E9%99%90%E6%B5%81.jpg?alt=media)

![Middleware的顺序](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-91f6e5ea13436111df2cf7be68a24c4d7f2326c8%2FMiddleware%E7%9A%84%E9%A1%BA%E5%BA%8F.png?alt=media)

## PART3. Middleware要不要考虑分路由问题?

前面所有的Middleware都是对所有请求生效的

一个很常见的场景:

我们希望区分不同的路由,进行不同的处理.例如公开页面,用户不需要登录,但是有一些页面,用户就需要登录

**这TM是作业**

## PART4. 面试要点

1. **什么是可观测性?**
   * 也就是logging、metrics和tracing
2. **常用的可观测性的框架有哪些?**
   * 你举例自己公司用的,或者开源的OpenTelemetry、SkyWalking、Prometheus都可以
3. **怎么集成可观测性框架?**
   * 一般都是利用Middleware机制(AOP),不仅仅是Web框架,几乎所有的框架都有类似Middleware的机制
4. **Prometheus的Histogram和Summary?**
   * 见[第2周第17课:Prometheus详解](https://github.com/rayallen20/GoInAction/blob/master/note/%E7%AC%AC2%E5%91%A8-Web%E6%A1%86%E6%9E%B6%E4%B9%8BContext%E4%B8%8EAOP%E6%96%B9%E6%A1%88/PART3.%20Middleware/17.%20Prometheus%E8%AF%A6%E8%A7%A3.md)
5. **全链路追踪(tracing)的几个概念?**
   * 解释一下tracer、tracing和span的概念.主要是span如何构建成树的
6. **tracing是怎么构建的?**
   * 核心在于解释清楚tracing进程内和跨进程的运作,我们将在微服务框架里面看到跨进程是怎么处理的
7. **HTTP应该观测一些什么数据?**
   * 也就是我们OpenTelemetry和Promtheus两个Middleware里面写的那些指标
8. **什么是99线、999线?**
   * 就是响应的比例,99%的响应、99.9%的响应的响应时间在多少ms以内


# PART08.Review


# 8.01 课后复习-AOP

对责任链模式不太了解的读者建议先翻看一下[责任链模式简介](https://github.com/step-by-step-wiki/GoBook/blob/main/PART09.Appendix/附录1.责任链模式.md)

## PART1. 非集中式的设计方案--洋葱模式

### 1.1 定义Middleware

`middleware.go`:

```go
package web

// Middleware 中间件
type Middleware func(HandleFunc) HandleFunc
```

这种入参和返回值均为函数类型的设计,是函数式编程.

### 1.2 编排Middleware的顺序

实际上这一步就是将多个Middleware组建成一条责任链

#### 1.2.1 定义中间件链

`httpServer.go`:

```go
package web

import (
	"net"
	"net/http"
)

// HTTPServer HTTP服务器
type HTTPServer struct {
	router                   // router 路由树
	middlewares []Middleware // middlewares Server级别的中间件链 实际上就是责任链 所有的请求都会经过这个链的处理
}
```

#### 1.2.2 构建中间件链

需要注意的是,构建中间件链的顺序,和中间件的执行顺序是相反的.换言之,也就是说最后一个被执行的中间件是最先被组装到责任链上的

`httpServer.go`:

```go
package web

import (
	"net"
	"net/http"
)

// HTTPServer HTTP服务器
type HTTPServer struct {
	router                   // router 路由树
	middlewares []Middleware // middlewares Server级别的中间件链 实际上就是责任链 所有的请求都会经过这个链的处理
}

// ServeHTTP WEB框架入口
func (s *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// 构建上下文
	ctx := &Context{
		Req:  r,
		Resp: w,
	}

	// 构建责任链
	// step1. 找到请求对应的HandleFunc
	root := s.serve
	for i := len(s.middlewares) - 1; i >= 0; i-- {
		// step2. 从后往前构建责任链
		// Tips: 组装的过程是从后向前的 也就是说最后一个被执行的中间件是最先被组装到责任链上的
		root = s.middlewares[i](root)
	}

	// 从责任链的头部开始执行
	root(ctx)
}
```

这里之所以要先找到请求对应的HandleFunc,是因为请求在经过了整条中间件链后,最终还是要去执行这个HandleFunc.

你可能有所疑问的地方在于:如果我找不到请求对应的HandleFunc,那为什么还要走这一整条中间件链?

我个人的理解是:**因为这条中间件链是Server级别的**.就如同Nginx的access.log,即使访问了一个不存在的url,同样也会在access.log中记录一条404的日志

## PART2. 测试

`middleware_test.go`:

```go
package web

import (
	"fmt"
	"net/http"
	"testing"
)

// Test_Middleware 测试中间件的工作顺序
func Test_Middleware(t *testing.T) {
	s := NewHTTPServer()

	s.middlewares = []Middleware{
		Middleware1,
		Middleware2,
		Middleware3,
		Middleware4,
	}

	s.ServeHTTP(nil, &http.Request{})
}

func Middleware1(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件1开始执行")
		next(ctx)
		fmt.Println("中间件1结束执行")
	}
}

func Middleware2(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件2开始执行")
		next(ctx)
		fmt.Println("中间件2结束执行")
	}
}

func Middleware3(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件3中断后续中间件的执行")
	}
}

func Middleware4(next HandleFunc) HandleFunc {
	return func(ctx *Context) {
		fmt.Println("中间件4不会被执行")
	}
}
```

测试结果:

```
中间件1开始执行
中间件2开始执行
中间件3中断后续中间件的执行
中间件2结束执行
中间件1结束执行
```


# 8.02 课后复习-Context

## PART1. 对Body的输入进行JSON反序列化

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
)

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

// BindJSON 绑定请求体中的JSON数据到给定的目标对象上 这个目标对象可能是某个结构体的实例 也有可能是个map
func (c *Context) BindJSON(target any) error {
	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if c.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	decoder := json.NewDecoder(c.Req.Body)
	return decoder.Decode(target)
}
```

这里之所以不用`json.Unmarshal()`,是因为用这个API多一步将`io.Reader`接口转换为`[]byte`的过程:

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"io"
	"net/http"
)

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

// JSONUnmarshal 绑定请求体中的JSON数据到给定的目标对象上 这个目标对象可能是某个结构体的实例 也有可能是个map
// Tips: 该方法与BindJSON方法的区别在于该方法使用了 json.Unmarshal 方法
func (c *Context) JSONUnmarshal(target any) error {
	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if c.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	bytes, err := io.ReadAll(c.Req.Body)
	if err != nil {
		return err
	}

	return json.Unmarshal(bytes, target)
}
```

## PART2. 处理表单输入

1. 无论你后续准备使用`http.Request.Form`还是`http.Request.PostForm`,都得先调用`http.ParseForm()`方法解析表单数据,这俩字段才有值
2. `http.Request.FormValue()`方法内部虽然会调用`http.ParseForm()`方法解析表单数据,但是这个方法不抛出error,所以最好事前手动调用一次
3. 我在复习时,将检测给定的key是否存在也作为了一个判断错误的条件

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
)

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

// FormValue 获取表单中给定的key对应的值
func (c *Context) FormValue(key string) (value string, err error) {
	err = c.Req.ParseForm()
	if err != nil {
		return "", errors.New("web绑定错误: 解析表单失败: " + err.Error())
	}

	_, ok := c.Req.Form[key]
	if !ok {
		return "", errors.New("web绑定错误: 表单中没有给定的key: " + key)
	}

	return c.Req.FormValue(key), nil
}
```

## PART3. 处理查询参数

### 3.1 基本实现

`context.go`:

```gopackage

import (
	"encoding/json"
	"errors"
	"net/http"
)

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

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	return c.Req.URL.Query().Get(key), nil
}
```

### 3.2 缓存查询参数

多次调用`http.Request.URL.Query()`会重复解析URL,因此需要将查询参数缓存起来

注意,这里缓存参数时,不要使用`map[string][]string`的数据类型,虽然它和`url.Values`是同一种类型,因为后边的代码写的会非常难受:

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
)

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

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	values, ok := c.queryValue[key]
	if !ok {
		return "", errors.New("web绑定错误: URL中没有给定的key: " + key)
	}

	if len(values) == 0 {
		return "", errors.New("web绑定错误: URL中给定的key对应的值为空")
	}

	return values[0], nil
}
```

就保持和API一致,还使用`url.Values`即可.原因有2个:

1. 这里将`queryValue`设置成私有字段,意味着它的作用域不会离开`web`包,所以它和框架使用者的代码是隔离的,只能通过`Context. QueryValue()`方法来访问`queryValue`中的K-V(确切的说只能访问V)
2. 后续通过Key查找Value时,`url.Values`类型是有API的(当然最终的版本并没有使用这个API),而如果使用`map[string][]string`,则必须自行判断:
   * key是否存在
   * key对应的value是否为空slice

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	return c.queryValue.Get(key), nil
}
```

这里需要注意的是,这个缓存是不存在失效和不一致的问题的.因为一旦服务端接收到一个请求,那么这个请求的参数就是固定的了

### 3.3 无法区分key不存在的情况与key对应的参数值是空字符串的情况

注意`url.Values.Get()`方法的返回值类型为string,所以即使没有找到key对应的查询参数,它依然返回一个空字符串.所以其实到底用`url.Values`还是`map[string][]string`其实区别并不大

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value string, err error) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	values, ok := c.queryValue[key]
	if !ok {
		return "", errors.New("web绑定错误: URL中没有给定的key: " + key)
	}

	if len(values) == 0 {
		return "", errors.New("web绑定错误: URL中给定的key没有对应的值: " + key)
	}

	return values[0], nil
}
```

## PART4. 处理路径参数

路径参数在查找路由树时就已经存起来了,这里只需要取就行

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// PathValue 获取路径参数中给定的key对应的值
func (c *Context) PathValue(key string) (value string, err error) {
	if c.PathParams == nil {
		return "", errors.New("web绑定错误: 路径参数为空")
	}

	value, ok := c.PathParams[key]
	if !ok {
		return "", errors.New("web绑定错误: 路径中没有给定的key: " + key)
	}

	return value, nil
}
```

## PART5. 返回不同数据类型的输入

思路:定义一个结构体,该结构体用于将string类型的输入转换为不同的类型;以上所有方法不再直接返回string类型的值,而是返回一个该结构体idea实例.**注意这里返回的是实例而非指针,因为这个结构体中的属性不该被修改,换言之该结构体是"只读"的**.

### 5.1 定义ReqValue结构体

`reqValue.go`:

```go
package web

import "strconv"

// ReqValue 用于承载来自请求中各部分输入的值 并提供统一的类型转换API
type ReqValue struct {
	value string // value 来自请求中不同部分的值 以string类型表示
	err   error  // err 承载接收请求中的参数时出现的错误
}
```

### 5.2 定义各种类型转换的方法

`reqValue.go`:

```go
package web

import "strconv"

// ReqValue 用于承载来自请求中各部分输入的值 并提供统一的类型转换API
type ReqValue struct {
	value string // value 来自请求中不同部分的值 以string类型表示
	err   error  // err 承载接收请求中的参数时出现的错误
}

// AsInt64 将ReqValue中的值转换为int64类型
func (r ReqValue) AsInt64() (value int64, err error) {
	if r.err != nil {
		return 0, r.err
	}

	return strconv.ParseInt(r.value, 10, 64)
}

// AsUint64 将ReqValue中的值转换为uint64类型
func (r ReqValue) AsUint64() (value uint64, err error) {
	if r.err != nil {
		return 0, r.err
	}

	return strconv.ParseUint(r.value, 10, 64)
}

// AsFloat64 将ReqValue中的值转换为float64类型
func (r ReqValue) AsFloat64() (value float64, err error) {
	if r.err != nil {
		return 0, r.err
	}

	return strconv.ParseFloat(r.value, 64)
}
```

### 5.3 context的各个处理请求参数的方法中返回ReqValue

`context.go`:

```go
package web

import (
	"encoding/json"
	"errors"
	"net/http"
	"net/url"
)

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

// BindJSON 绑定请求体中的JSON数据到给定的目标对象上 这个目标对象可能是某个结构体的实例 也有可能是个map
func (c *Context) BindJSON(target any) error {
	if target == nil {
		return errors.New("web绑定错误: 给定的实例为空")
	}

	if c.Req.Body == nil {
		return errors.New("web绑定错误: 请求体为空")
	}

	decoder := json.NewDecoder(c.Req.Body)
	return decoder.Decode(target)
}

// FormValue 获取表单中给定的key对应的值
func (c *Context) FormValue(key string) (value ReqValue) {
	err := c.Req.ParseForm()
	if err != nil {
		return ReqValue{err: err}
	}

	_, ok := c.Req.Form[key]
	if !ok {
		return ReqValue{err: errors.New("web绑定错误: 表单中没有给定的key: " + key)}
	}

	return ReqValue{value: c.Req.FormValue(key)}
}

// QueryValue 获取URL中给定的key对应的值
func (c *Context) QueryValue(key string) (value ReqValue) {
	if c.queryValue == nil {
		c.queryValue = c.Req.URL.Query()
	}

	values, ok := c.queryValue[key]
	if !ok {
		return ReqValue{err: errors.New("web绑定错误: URL中没有给定的key: " + key)}
	}

	if len(values) == 0 {
		return ReqValue{err: errors.New("web绑定错误: URL中给定的key没有对应的值: " + key)}
	}

	return ReqValue{value: values[0]}
}

// PathValue 获取路径参数中给定的key对应的值
func (c *Context) PathValue(key string) (value ReqValue) {
	if c.PathParams == nil {
		return ReqValue{err: errors.New("web绑定错误: 路径参数为空")}
	}

	val, ok := c.PathParams[key]
	if !ok {
		return ReqValue{err: errors.New("web绑定错误: 路径中没有给定的key: " + key)}
	}

	return ReqValue{value: val}
}
```

### 5.4 测试用例

`context_test.go`:

```go
package web

import (
	"fmt"
	"net/http"
	"testing"
)

func Test_Context(t *testing.T) {
	server := &HTTPServer{router: newRouter()}

	handleFunc := func(c *Context) {
		id, err := c.PathValue("id").AsInt64()
		if err != nil {
			c.Resp.WriteHeader(http.StatusBadRequest)
			_, _ = c.Resp.Write([]byte("id输入不正确: " + err.Error()))
		}

		_, _ = c.Resp.Write([]byte(fmt.Sprintf("id: %d", id)))
	}

	server.GET("/order/:id", handleFunc)
	_ = server.Start(":8091")
}
```


# 8.03 课后复习-Middleware-AccessLog

对生成器模式不太了解的读者建议先翻看一下[生成器模式](https://github.com/step-by-step-wiki/GoBook/blob/main/PART09.Appendix/附录2.生成器模式.md)

## PART1. 使用生成器模式创建Middleware

`middlewares/accessLog/middlewareBuilder.go`:

```go
package accessLog

import "web"

// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct{}

// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			next(ctx)
			// 在这里记录日志 例如:命中的路由/HTTP动词/请求参数等
		}
	}
}
```

Tips:相比于示例,这里只是没有定义`IBuilder`接口,本质上还是为了调用不同的`Build()`函数能够得到不同"特征"(或者也可以说是不同功能)的中间件.我个人觉得这里定义接口,意义不大.原因:这个场景下没有示例中的`Director`类.换言之,这个场景下没有哪个类负责"编排中间件的构建过程",因为每个中间件在实例化时所需的字段或函数是不同的,没有办法让某个类去针对所有的中间件统一完成这个过程

## PART2. 定义中间件结构

记录如下内容:

* 请求主机地址
* 命中的路由
* 请求的HTTP动词
* 请求的uri(也就是路径)

`middlewares/accessLog/accessLog.go`:

```go
package accessLog

// accessLog 本结构体用于定义日志内容
type accessLog struct {
	Host       string `json:"host,omitempty"`        // Host 请求的主机地址
	Route      string `json:"route,omitempty"`       // Route 命中的路由
	HTTPMethod string `json:"http_method,omitempty"` // HTTPMethod 请求的HTTP动词
	Path       string `json:"path,omitempty"`        // Path 请求的路径 即:uri
}
```

## PART3. 获取要记录的字段值

### 3.1 基本实现

`middlewares/accessLog/middlewareBuilder.go`:

```go
package accessLog

import "web"

// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct{}

// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 构建日志内容
			defer func() {
				log := accessLog{
					Host:       ctx.Req.Host,
					Route:      "",
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}
				
				// TODO: 写入日志
			}()
			next(ctx)
		}
	}
}
```

这里把记录的日志放在defer中的原因:

1. 直接写在`next(ctx)`之后不合适,因为`next(ctx)`意味着执行责任链上的后续中间件.在后续中间件的执行过程中有可能出现panic,那样的话整个进程就结束了,因此写在`next(ctx)`之后的代码也就无法被执行
2. 写在`next(ctx)`之前,也不太合适.2个原因:
   * 如果请求没有命中任何路由,有可能就不需要记录日志了,写在`next(ctx)`之前就意味着无论是否命中路由都记录日志
   * 如果在记录日志的过程中出现panic,就会因为这些非关键流程影响到关键流程

因此写在defer中最合适

### 3.2 记录命中的路由

这里的问题在于,不能直接从命中的节点上取路由,因为命中节点上的路由只是全路由中的最后一段,而非全路由.因此这里修改的思路是:

* **新增路由时,在对应的节点(即HandleFunc所在的节点)上记录全路由**
* **匹配路由时,查找到对应节点后,将节点的全路由记录到Context上**

#### step1. Context结构体增加用于记录命中的路由的字段

`context.go`:

```go
// Context HandleFunc的上下文
type Context struct {
	Req        *http.Request       // Req 请求
	Resp       http.ResponseWriter // Resp 响应
	PathParams map[string]string   // PathParams 路径参数名值对
	queryValue url.Values          // queryValue 查询参数名值对
	MatchRoute string              // MatchRoute 请求命中的路由
}
```

#### step2. Node结构体中增加用于记录命中该节点时的全路由的字段

`node.go`:

```go
package web

import (
	"fmt"
	"strings"
)

// node 路由树的节点
type node struct {
	path          string           // path 当前节点的路径
	children      map[string]*node // children 子路由路径到子节点的映射
	wildcardChild *node            // wildcardChild 通配符子节点
	paramChild    *node            // paramChild 参数子节点
	HandleFunc                     // HandleFunc 路由对应的业务逻辑
	fullRoute     string           // fullRoute 命中该节点时的完整路由
}
```

#### step3. 添加节点时记录全路由

在原先基础上,创建节点并设置HandleFunc后,设置全路由即可

`router.go`:

```go
// addRoute 注册路由到路由森林中的路由树上
func (r *router) addRoute(method string, path string, handleFunc HandleFunc) {
	if path == "" {
		panic("web: 路由不能为空字符串")
	}

	if path[0] != '/' {
		panic("web: 路由必须以 '/' 开头")
	}

	if path != "/" && path[len(path)-1] == '/' {
		panic("web: 路由不能以 '/' 结尾")
	}

	root, ok := r.trees[method]
	if !ok {
		root = &node{
			path: "/",
		}
		r.trees[method] = root
	}

	if path == "/" {
		if root.HandleFunc != nil {
			panic("web: 路由冲突,重复注册路由 [/] ")
		}
		root.HandleFunc = handleFunc

		// 记录节点的全路由
		root.fullRoute = path
		return
	}

	path = strings.TrimLeft(path, "/")
	segments := strings.Split(path, "/")

	target := root
	for _, segment := range segments {
		if segment == "" {
			panic("web: 路由中不得包含连续的'/'")
		}

		child := target.childOrCreate(segment)
		target = child
	}

	if target.HandleFunc != nil {
		panic(fmt.Sprintf("web: 路由冲突,重复注册路由 [%s] ", path))
	}

	target.HandleFunc = handleFunc

	// 记录节点的全路由
	target.fullRoute = path
}
```

#### step4. 查找到节点后将节点的全路由赋值给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)
	if !ok || targetNode.node.HandleFunc == nil {
		ctx.Resp.WriteHeader(http.StatusNotFound)
		_, _ = ctx.Resp.Write([]byte("Not Found"))
		return
	}
	
	ctx.PathParams = targetNode.pathParams
	
	// 命中节点则将节点的全路由设置到上下文中
	ctx.MatchRoute = targetNode.node.fullRoute
	
	targetNode.node.HandleFunc(ctx)
}
```

#### step5. 中间件中从Context中取值即可

`middlewares/accessLog/middlewareBuilder.go`:

```go
package accessLog

import "web"

// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct{}

// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 构建日志内容
			defer func() {
				log := accessLog{
					Host:       ctx.Req.Host,
					Route:      ctx.MatchRoute,
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}

				// TODO: 写入日志
			}()
			next(ctx)
		}
	}
}
```

## PART4. 写日志操作

### 4.1 定义记录日志的函数

这里不要直接写死(写成`log.Print()`之类的),因为框架的使用者不一定想要以这种方式记录日志.这里的关键点在于:**把定义记录日志过程的能力,交给框架的使用者**

`middlewares/accessLog/middlewareBuilder.go`:

```go
package accessLog

import (
	"encoding/json"
	"web"
)

// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct {
	logFunc func(content string) // logFunc 用于记录日志的函数
}

// SetLogFunc 本方法用于设置记录日志的函数
func (b *AccessMiddlewareBuilder) SetLogFunc(logFunc func(string)) {
	b.logFunc = logFunc
}
```

### 4.2 调用记录日志的函数

`middlewares/accessLog/middlewareBuilder.go`:

```go
package accessLog

import (
	"encoding/json"
	"web"
)

// AccessMiddlewareBuilder 日志中间件构建器
type AccessMiddlewareBuilder struct {
	logFunc func(content string) // logFunc 用于记录日志的函数
}

// SetLogFunc 本方法用于设置记录日志的函数
func (b *AccessMiddlewareBuilder) SetLogFunc(logFunc func(string)) {
	b.logFunc = logFunc
}

// Build 本方法用于构建一个日志中间件
func (b *AccessMiddlewareBuilder) Build() web.Middleware {
	return func(next web.HandleFunc) web.HandleFunc {
		return func(ctx *web.Context) {
			// 构建日志内容
			defer func() {
				log := accessLog{
					Host:       ctx.Req.Host,
					Route:      ctx.MatchRoute,
					HTTPMethod: ctx.Req.Method,
					Path:       ctx.Req.URL.Path,
				}

				// 记录日志
				logJsonBytes, _ := json.Marshal(log)
				b.logFunc(string(logJsonBytes))
			}()
			next(ctx)
		}
	}
}
```

## PART5. 传递中间件

对函数选项模式不太了解的读者建议先翻看一下[函数选项模式](https://github.com/rayallen20/GoInAction/blob/master/note/%E7%AC%AC2%E5%91%A8-Web%E6%A1%86%E6%9E%B6%E4%B9%8BContext%E4%B8%8EAOP%E6%96%B9%E6%A1%88/PART5.%20Appendix/%E9%99%84%E5%BD%953.%20%E5%87%BD%E6%95%B0%E9%80%89%E9%A1%B9%E6%A8%A1%E5%BC%8F.md)

### 5.1 定义选项函数类型

`option.go`:

```go
package web

// Option 本类型为 HttpServer 的选项函数
// 本类型的每个不同实例均用于修改 HttpServer 的不同字段值
type Option func(server *HTTPServer)
```

### 5.2 定义With()函数

`httpServer.go`:

```go
// ServerWithMiddlewares 本函数用于根据给定的 Middleware 列表,创建
// 修改 HttpServer 实例的 middlewares 字段值的选项函数
func ServerWithMiddlewares(middlewares ...Middleware) Option {
	return func(server *HTTPServer) {
		server.middlewares = middlewares
	}
}
```

### 5.3 实例化HttpServer时根据选项函数修改成员属性的值

`httpServer.go`:

```go
// NewHTTPServer 根据给定的 Option 列表(每个 Option 均表示要修改一个 HttpServer 的成员属性),创建HTTP服务器
func NewHTTPServer(options ...Option) *HTTPServer {
	httpServer := &HTTPServer{
		router: newRouter(),
	}

	for _, option := range options {
		option(httpServer)
	}

	return httpServer
}
```

Tips:相比于示例,这里也没有使用`IOption`接口,因为意义也不大.面向接口编程本质上是为了面向客户端代码时隐藏具体实现,这里我认为如果定义了`IOption`接口,事情反而更加复杂了,因为你还要定义这个接口的各种不同实现,例如`MiddlewareOption`,`PortOption`(这里我们假定`HTTPSserver`还有一个名为`port`的字段)等

## PART6. 测试

`middleware_test.go`:

```go
package accessLog

import (
	"fmt"
	"testing"
	"web"
)

// Test_Middleware 本函数用于测试 accessLog 是否工作正常
func Test_Middleware(t *testing.T) {
	// step1. 创建中间件
	logFunc := func(content string) {
		fmt.Printf("%#v\n", content)
	}
	middlewareBuilder := AccessMiddlewareBuilder{
		logFunc: logFunc,
	}
	accessLogMiddleware := middlewareBuilder.Build()

	// step2. 创建HTTPServer
	middlewareOption := web.ServerWithMiddlewares(accessLogMiddleware)
	httpServer := web.NewHTTPServer(middlewareOption)

	// step3. 启动HTTPServer
	handleFunc := func(ctx *web.Context) {
		ctx.Resp.Write([]byte("hello"))
	}
	httpServer.GET("/user/show", handleFunc)

	httpServer.Start(":8081")
}
```


# PART09.Appendix


# 附录1.责任链模式

责任链模式是一种**行为设计模式**,允许你将请求沿着处理者链进行发送,直至其中一个处理者对该请求进行处理.

该模式允许多个对象来对请求进行处理,而无需让发送者类与具体接收者类相耦合.链可在运行时由**遵循标准处理者接口的任意处理者**动态生成.

## PART1. 概念示例

### 1.1 背景故事

让我们来看看一个医院应用的责任链模式例子.医院中会有多个部门,如:

* 前台
* 医生
* 药房
* 收银

病人来访时,他们首先都会去前台,然后是看医生、取药,最后结账.也就是说,病人需要通过一条部门链,**每个部门都在完成其职能后将病人进一步沿着链条输送**.

此模式适用于**有多个候选选项处理相同请求**的情形,适用于**不希望客户端选择接收者**(因为多个对象都可处理请求)的情形,还适用于想**将客户端同接收者解耦**时.客户端只需要链中的首个元素即可.

正如示例中的医院,患者在到达后首先去的就是前台.然后根据患者的当前状态,前台会将其指向链上的下一个处理者.

### 1.2 病人结构体

`pattern/patient.go`:

```go
package pattern

// Patient 病人结构体
type Patient struct {
	Name              string // 姓名
	RegistrationDone  bool   // 是否完成挂号
	DoctorCheckUpDone bool   // 是否完成医生检查
	MedicineDone      bool   // 是否完成取药
	PaymentDone       bool   // 是否完成缴费
}
```

### 1.3 处理者接口

`pattern/department/department.go`:

```go
package department

import "responsibilityChain/pattern"

type Department interface {
	Execute(patient *pattern.Patient) // Execute 用于让病人执行当前部门的处理
	SetNext(department Department)    // SetNext 设置下一个部门
}
```

### 1.4 挂号部门结构体

`pattern/department/reception.go`:

```go
package department

import (
	"fmt"
	"responsibilityChain/pattern"
)

// Reception 挂号部门
type Reception struct {
	next Department // next 挂号部门处理病人完成后,即将处理病人的下一个部门
}

// Execute 本方法用于执行挂号部门处理病人的过程 并调用下一个处理病人部门的执行逻辑
func (r *Reception) Execute(patient *pattern.Patient) {
	if patient.RegistrationDone {
		fmt.Printf("patient registration already done\n")
		if r.next != nil {
			r.next.Execute(patient)
		}
		return
	}

	fmt.Printf("Reception registering patient now\n")
	patient.RegistrationDone = true
	if r.next != nil {
		r.next.Execute(patient)
	}
	return
}

// SetNext 设置下一个处理病人的部门
func (r *Reception) SetNext(department Department) {
	r.next = department
}
```

### 1.5 医生部门结构体

`pattern/department/doctor.go`:

```go
package department

import (
	"fmt"
	"responsibilityChain/pattern"
)

// Doctor 医生部门
type Doctor struct {
	next Department // next 医生部门处理病人完成后,即将处理病人的下一个部门
}

// Execute 本方法用于执行医生部门处理病人的过程 并调用下一个处理病人部门的执行逻辑
func (d *Doctor) Execute(patient *pattern.Patient) {
	if patient.DoctorCheckUpDone {
		fmt.Printf("Doctor checkup already done\n")
		if d.next != nil {
			d.next.Execute(patient)
			return
		}
	}

	fmt.Printf("Doctor checking patient\n")
	patient.DoctorCheckUpDone = true
	if d.next != nil {
		d.next.Execute(patient)
	}
	return
}

// SetNext 设置下一个处理病人的部门
func (d *Doctor) SetNext(next Department) {
	d.next = next
}
```

### 1.6 药房部门结构体

`pattern/department/medical.go`:

```go
package department

import (
	"fmt"
	"responsibilityChain/pattern"
)

// Medical 药房部门
type Medical struct {
	next Department // next 药房部门处理病人完成后,即将处理病人的下一个部门
}

// Execute 本方法用于执行药房部门处理病人的过程 并调用下一个处理病人部门的执行逻辑
func (m *Medical) Execute(patient *pattern.Patient) {
	if patient.MedicineDone {
		fmt.Printf("Medical already given to patient\n")
		if m.next != nil {
			m.next.Execute(patient)
			return
		}
	}

	fmt.Printf("Medical giving to patient\n")
	patient.MedicineDone = true
	if m.next != nil {
		m.next.Execute(patient)
	}

	return
}

// SetNext 设置下一个处理病人的部门
func (m *Medical) SetNext(next Department) {
	m.next = next
}
```

### 1.7 收费部门结构体

`pattern/department/cashier.go`:

```go
package department

import (
	"fmt"
	"responsibilityChain/pattern"
)

// Cashier 收费部门
type Cashier struct {
	next Department // next 收费部门处理病人完成后,即将处理病人的下一个部门
}

// Execute 本方法用于执行收费部门处理病人的过程 并调用下一个处理病人部门的执行逻辑
func (c *Cashier) Execute(patient *pattern.Patient) {
	if patient.PaymentDone {
		fmt.Printf("Payment Done\n")

		if c.next != nil {
			c.next.Execute(patient)
			return
		}
	}

	fmt.Printf("Cashier getting money from patient patient\n")
	patient.PaymentDone = true
	if c.next != nil {
		c.next.Execute(patient)
	}

	return
}

// SetNext 设置下一个处理病人的部门
func (c *Cashier) SetNext(department Department) {
	c.next = department
}
```

### 1.8 客户端调用

`main.go`:

```go
package main

import (
	"responsibilityChain/pattern"
	"responsibilityChain/pattern/department"
)

func main() {
	patient := &pattern.Patient{
		Name: "abc",
	}

	departmentChain := initDepartmentChain()
	departmentChain.Execute(patient)
}

// initDepartmentChain 本函数用于初始化部门链并返回位于链首部的第1个部门
// Tips: 注意部门初始化的顺序和部门处理病人的顺序是相反的
func initDepartmentChain() department.Department {
	// 初始化收费部门
	cashier := &department.Cashier{}

	// 初始化药房部门
	medical := &department.Medical{}
	medical.SetNext(cashier)

	// 初始化医生部门
	doctor := &department.Doctor{}
	doctor.SetNext(medical)

	// 初始化挂号部门
	reception := &department.Reception{}
	reception.SetNext(doctor)

	return reception
}
```

执行结果:

```
Reception registering patient now
Doctor checking patient
Medical giving to patient
Cashier getting money from patient patient
```

需要注意的是,各个部门的初始化的顺序,与其在责任链上的顺序是相反的;初始化的顺序与执行的顺序也是相反的,先初始化的收费部门,但收费部门的`Execute()`方法最后执行

## PART2. 示例相关UML

![责任链模式UML](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-7fd13504ebfc6b1ea00cca2016f09689c3ed0df9%2F%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8FUML.jpg?alt=media)

## PART3. 责任链模式工作流程

### 3.1 责任链的组装与执行顺序

![责任链的组装与执行顺序](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-d93f60ef2059e4d751e9478c1983c85087340171%2F%E8%B4%A3%E4%BB%BB%E9%93%BE%E7%9A%84%E7%BB%84%E8%A3%85%E4%B8%8E%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F.jpg?alt=media)

### 3.2 责任链的详细执行顺序

![责任链模式的详细执行顺序](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-aa31179b0cca243ceecfae9e16f2998e3b92f46a%2F%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F%E7%9A%84%E8%AF%A6%E7%BB%86%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F.jpg?alt=media)

## PART4. 责任链模式的使用场景

1. **如果程序需要使用不同的方式处理不同种类的请求,并且请求类型和顺序预先不确定时,可以使用责任链模式**.责任链模式可以将多个具体的处理者对象连接成一条链,在接收到请求后,责任链模式会询问每个具体的请求者能否对该请求进行处理,因此所有的处理者对象都有机会处理请求
2. **如果必须按顺序执行多个具体的处理者对象,则可以使用责任链模式**.无论以何种顺序将具体的处理者对象连接成一条链,所有请求都会严格按顺序通过链中的具体处理者对象
3. **如果所需的具体处理者对象及其顺序必须在运行时发生改变(或者也可以说必须在运行时才能确定下来),则可以使用责任链模式**.如果在具体的处理者类中存在对成员变量的引用,那么开发者可以动态地插入和删除具体处理者对象或改变其顺序.
   * 说白了就是当需要改变各个元素在责任链上的顺序时,调整每个元素的`next`成员属性,使该成员属性引用不同的具体处理者对象即可

## PART5. 责任链模式的优缺点

### 5.1 优点

1. 开发者可以控制请求处理的顺序
2. 符合单一职责原则(SRP,Single Responsibility Principle).开发者可以对发起操作的类和执行操作的类进行解耦(发起操作和执行操作是2个不同的职责)
3. 符合开闭原则(开放扩展,闭合修改).开发者可以在**不更改现有代码的情况下**,在程序中添加处理者对象
4. 提高对象分配职责的灵活性.通过更改链中的成员或更改其顺序,允许动态添加或删除处理者对象.增加新的处理者类时很方便
5. 责任链模式可以简化对象,使得对象不需要知道链的结构

### 5.2 责任链模式的缺点

1. 责任链模式的请求不保证一定能被收到,部分请求可能未被处理
   * 责任链模式不保证请求一定到达链首部的第1个处理者对象
2. 系统的性能会受到影响,而且在代码调试不方便时可能会造成循环调用.例如:
   * 如果某个处理者对象未能成功调用下一个处理者的处理方法,那么请求可能会被丢弃
   * 如果一个处理者对象调用了不正确的处理者的处理方法,那么可能损害责任链的结构
3. 在调试时,不容易观察操作特性
4. 责任链模式有时会增加维护成本,因为不同的处理程序中可能会出现重复的代码


# 附录2.生成器模式

生成器模式是一种**创建型设计模式**,使你能够**分步骤创建复杂对象**.

与其他创建型模式不同,**生成器模式不要求产品拥有通用接口**.这使得**用相同的创建过程生成不同的产品**成为可能.

## PART1. 概念示例

当所需产品较为复杂且需要多个步骤才能完成时,可以使用生成器模式.在这种情况下,**使用多个构造方法比仅仅使用一个复杂可怕的构造函数更简单**.但分多个步骤进行构建的策略也有其问题.分为多个步骤进行构建的潜在问题在于:构建不完整的和不稳定的产品可能会被暴露给客户端.生成器模式能够在产品完成构建之前使其处于私密状态.

### 1.1 背景故事

现有2种房屋:

* 冰屋
* 普通房屋

这两种房屋的建造步骤都是相同的(例如先装窗户、再装门,最后装地板).该如何组织这个建造过程?

### 1.2 房屋结构体

房屋结构体实际上就是我们完成所有建造步骤后,最终对外提供的产品

`pattern/house.go`:

```go
package pattern

// House 房屋结构体 即最终对外提供的产品
type House struct {
	windowType string // windowType 窗户类型
	doorType   string // doorType 门的类型
	floor      int    // floor 房屋楼层数
}

// GetWindowType 本方法用于获取房屋的窗户类型
func (h House) GetWindowType() string {
	return h.windowType
}

// GetDoorType 本方法用于获取房屋的门的类型
func (h House) GetDoorType() string {
	return h.doorType
}

// GetFloor 本方法用于获取房屋的楼层数
func (h House) GetFloor() int {
	return h.floor
}
```

### 1.3 生成器接口

接口定义行为.本例中的生成器接口定义了创建一个房屋所必须经历的过程,也就是创建一个房屋的行为

`pattern/iBuilder.go`

```go
package pattern

// IBuilder 生成器接口 本接口用于定义创建房屋的必要过程
type IBuilder interface {
	setWindowType()  // setWindowType 本方法用于设置窗户类型
	setDoorType()    // setDoorType 本方法用于设置门的类型
	setFloor()       // setFloor 本方法用于设置房屋楼层数
	getHouse() House // getHouse 本方法用于创建并返回房屋
}
```

### 1.4 普通房屋生成器

`pattern/normalBuilder.go`:

```go
package pattern

// NormalBuilder 普通房屋生成器
type NormalBuilder struct {
	windowType string // windowType 普通房屋的窗户类型
	doorType   string // doorType 普通房屋的门的类型
	floor      int    // floor 普通房屋的房屋楼层数
}

// newNormalBuilder NormalBuilder 的构造函数
func newNormalBuilder() *NormalBuilder {
	return &NormalBuilder{}
}

// setWindowType 本方法用于设置普通房屋的窗户类型
func (b *NormalBuilder) setWindowType() {
	b.windowType = "木窗户"
}

// setDoorType 本方法用于设置普通房屋的门类型
func (b *NormalBuilder) setDoorType() {
	b.doorType = "木门"
}

// setFloor 本方法用于设置普通房屋的楼层数
func (b *NormalBuilder) setFloor() {
	b.floor = 2
}

// getHouse 本方法用于创建普通房屋
func (b *NormalBuilder) getHouse() House {
	return House{
		windowType: b.windowType,
		doorType:   b.doorType,
		floor:      b.floor,
	}
}
```

### 1.5 冰屋生成器

`pattern/iglooBuilder.go`:

```go
package pattern

// IglooBuilder 冰屋生成器
type IglooBuilder struct {
	windowType string // windowType 冰屋的窗户类型
	doorType   string // doorType 冰屋的门的类型
	floor      int    // floor 冰屋的房屋楼层数
}

// newIglooBuilder IglooBuilder 的构造函数
func newIglooBuilder() *IglooBuilder {
	return &IglooBuilder{}
}

// setWindowType 本方法用于设置冰屋的窗户类型
func (b *IglooBuilder) setWindowType() {
	b.windowType = "雪窗户"
}

// setDoorType 本方法用于设置冰屋的门类型
func (b *IglooBuilder) setDoorType() {
	b.doorType = "雪门"
}

// setFloor 本方法用于设置冰屋的楼层数
func (b *IglooBuilder) setFloor() {
	b.floor = 1
}

// getHouse 本方法用于创建冰屋
func (b *IglooBuilder) getHouse() House {
	return House{
		windowType: b.windowType,
		doorType:   b.doorType,
		floor:      b.floor,
	}
}
```

### 1.6 管理者结构体

以上两个具体的生成器类仅仅定义了建造一个房屋(无论是普通房屋还是冰屋)的每个具体过程,但还缺少一个角色,该角色用于**编排这些过程的执行顺序**.在本例中,可以理解为:无论是建造一个冰屋还是普通房屋,其建造顺序应该是固定的:

* step1. 造窗户
* step2. 造门
* step3. 确定楼层数

而管理者结构体的职责就是确定这些过程的执行顺序,并在这些过程都按顺序执行完毕后,返回被创建出的产品

`pattern/director.go`:

```go
package pattern

// Director 管理者结构体 本结构体用于编排生成器的生成过程 并最终生成产品
// 本结构体的存在使得客户端不必关心生成器的生成过程
type Director struct {
	builder IBuilder // builder 生成器接口的实现
}

// NewDirector Director 的构造函数
func NewDirector(builder IBuilder) *Director {
	return &Director{
		builder: builder,
	}
}

// SetBuilder 本方法用于为管理者结构体设置具体生成器
// 本方法存在的意义在于可以在运行时动态的改变管理者实例中的生成器实例
func (d *Director) SetBuilder(builder IBuilder) {
	d.builder = builder
}

// BuildHouse 本方法用于编排具体生成器的生成过程 并最终生成产品
func (d *Director) BuildHouse() House {
	d.builder.setWindowType()
	d.builder.setDoorType()
	d.builder.setFloor()
	return d.builder.getHouse()
}
```

### 1.7 提供对外暴露的创建具体生成器的方法

`pattern/IBuilder.go`:

```go
package pattern

// IBuilder 生成器接口 本接口用于定义创建房屋的必要过程
type IBuilder interface {
	setWindowType()  // setWindowType 本方法用于设置窗户类型
	setDoorType()    // setDoorType 本方法用于设置门的类型
	setFloor()       // setFloor 本方法用于设置房屋楼层数
	getHouse() House // getHouse 本方法用于创建并返回房屋
}

// GetBuilder 本函数用于根据给定的生成器类型 创建具体生成器
func GetBuilder(builderType string) IBuilder {
	switch builderType {
	case "normal":
		return newNormalBuilder()
	case "igloo":
		return newIglooBuilder()
	default:
		return nil
	}
}
```

### 1.8 客户端创建房屋

`main.go`:

```go
package main

import (
	"builder/pattern"
	"fmt"
)

func main() {
	// 普通房屋生成器实例
	normalBuilder := pattern.GetBuilder("normal")
	// 冰屋生成器实例
	iglooBuilder := pattern.GetBuilder("igloo")

	// 管理者实例
	director := pattern.NewDirector(normalBuilder)
	// 生成普通房屋
	normalHouse := director.BuildHouse()

	fmt.Printf("普通房屋的窗户类型: %s\n", normalHouse.GetWindowType())
	fmt.Printf("普通房屋的门的类型: %s\n", normalHouse.GetDoorType())
	fmt.Printf("普通房屋的楼层数: %d\n", normalHouse.GetFloor())

	// 重置管理者实例中的生成器实例
	director.SetBuilder(iglooBuilder)
	// 生成冰屋
	iglooHouse := director.BuildHouse()

	fmt.Printf("冰屋的窗户类型: %s\n", iglooHouse.GetWindowType())
	fmt.Printf("冰屋的门的类型: %s\n", iglooHouse.GetDoorType())
	fmt.Printf("冰屋的楼层数: %d\n", iglooHouse.GetFloor())
}
```

执行结果:

```
普通房屋的窗户类型: 木窗户
普通房屋的门的类型: 木门
普通房屋的楼层数: 2
冰屋的窗户类型: 雪窗户
冰屋的门的类型: 雪门
冰屋的楼层数: 1
```

## PART2. 示例相关UML

![生成器模式UML](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-1663a88f1f745d289c81435e9e5b5b8c70839efe%2F%E7%94%9F%E6%88%90%E5%99%A8%E6%A8%A1%E5%BC%8FUML.jpg?alt=media)

## PART3. 生成器模式工作流程

![生成器模式工作流程](https://1407465062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FyoSgzgsmuneFp7VNWUbE%2Fuploads%2Fgit-blob-f4dbe4896ffde656cff6c61a5e7b5bbcd0f7239f%2F%E7%94%9F%E6%88%90%E5%99%A8%E6%A8%A1%E5%BC%8F%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B.jpg?alt=media)

## PART4. 生成器模式使用场景

* 当开发者希望创建不同形式的产品时,可以使用生成器模式
* 如果开发者需要创建各种形式的产品,且这些产品的制造过程相似,且产品之间的差别不大(例如本例中的冰屋和普通房屋),可以使用生成器模式
* 如果需要使用构造函数,且构造函数中的参数很多时,可以使用生成器模式
* 当需要构建同一个对象的不同表示时,可以使用生成器模式.换言之,当需要创建具有不同特征的同一类对象时,可使用生成器模式

## PART5. 生成器模式的优缺点

### 5.1 优点

* 在生成器模式中,产品内部组成的细节对客户端不可见,将产品的**创建过程**和**产品自身**解耦,使**相同的创建过程可以创建不同的产品对象**
* 在生成器模式中,每个具体生成器都相对独立,因此可以十分方便地**替换具体生成器**或**增加新的具体生成器**,**且无须修改原有类库的代码(无侵入式的修改才是高明的修改)**,系统扩展方便,符合开闭原则(Open/Closed Principle,OCP),设计灵活性和代码可读性较高
* 生成器模式可以**将复杂产品的创建步骤分解在不同的方法中**,使创建过程更清晰,更易于使用程序控制创建过程

### 5.2 缺点

* 使用生成器模式创建的产品**组成部分都类似**,如果不同产品之间的差异很大(如很多组成部分都不相同),则不适合使用生成器模式,因此其使用范围有限
* 在生成器模式中,需要为不同类型的产品创建单独的具体生成器,因此代码量较大.如果系统比较大,则会增加系统的理解难度和运行成本


# 附录3.函数选项模式

## PART1. 概念示例

### 1.1 问题的产生

假设我们现在需要定义一个包含多个配置项的结构体,具体定义如下:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}
```

这个配置结构体中的字段可能有几个也可能有十几个,并且可能随着业务的发展不断增加新的字段.现在我们需要为其编写一个构造函数,根据经验我们可能会写出类似下面那样的构造函数:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

// NewSomeOption SomeOption 的构造函数
func NewSomeOption(a string, b int, c bool) *SomeOption {
	return &SomeOption{
		A: a,
		B: b,
		C: c,
	}
}
```

这样的设计面临着2个问题:

1. 如果`SomeOption`有十几个字段,那我们的构造函数需要定义十几个参数吗?如何为某些配置项指定默认值?
2. `SomeOption`随着业务发展不断新增字段后,我们的构造函数是否也需要同步变更?变更了构造函数是否又会影响既有代码?

### 1.2 函数选项模式

函数选项模式(Functional Options Pattern)也称为选项模式(Options Pattern),是一种**创建型设计模式**,选项模式允许你使用**接受0个或多个函数作为可变参数的构造函数**,以便构建复杂结构.我们将这些函数称为选项,由此得名函数选项模式

#### 1.2.1 可选参数

由于Go语言中的函数不支持默认参数,所以我们想到可以使用**可变长参数**来实现.而这个可变长参数的具体类型则需要好好设计一下.它必须满足以下条件:

* 不同的函数参数拥有相同的类型
* 指定函数参数能为特定的配置项赋值
* 支持扩展新的配置项

```go
package pattern

// OptionFunc 函数选项 该类型用于设置 SomeOption 的各属性值(即各选项)
type OptionFunc func(*SomeOption)
```

`OptionFunc`类型本质上是一个函数.该函数接收一个`*SomeOption`作为参数,并在其内部修改这个实例的字段值:

```go
package pattern

// OptionFunc 函数选项 该类型用于设置 SomeOption 的各属性值(即各选项)
type OptionFunc func(*SomeOption)

// WithB 本函数返回一个 OptionFunc ,该 OptionFunc 将
// SomeOption 实例的 B 字段值设置为给定值
func WithB(b int) OptionFunc {
	return func(someOption *SomeOption) {
		someOption.B = b
	}
}

// WithC 本函数返回一个 OptionFunc ,该 OptionFunc 将
// SomeOption 实例的 C 字段值设置为给定值
func WithC(c bool) OptionFunc {
	return func(someOption *SomeOption) {
		someOption.C = c
	}
}
```

那么这样一来,我们的构造函数就可以修改为:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

// NewSomeOption SomeOption 的构造函数
func NewSomeOption(a string, options ...OptionFunc) *SomeOption {
	someOption := &SomeOption{
		A: a,
	}

	for _, option := range options {
		option(someOption)
	}
	
	return someOption
}
```

最终创建`SomeOption`实例时的客户端代码就会简单很多:

```go
package main

import (
	"fmt"
	"option/pattern"
)

func main() {
	// 初始化SomeOption时,若SomeOption的字段有变化,仅需调整options切片即可
	options := []pattern.OptionFunc{
		pattern.WithB(10),
		pattern.WithC(true),
	}

	someOption := pattern.NewSomeOption("a", options...)
	fmt.Printf("%#v\n", someOption)
}
```

假如此时需求变更,不再需要初始化`SomeOption`实例时为`SomeOption.B`字段赋值,那么变更就会容易很多:

```go
package main

import (
	"fmt"
	"option/pattern"
)

func main() {
	options := []pattern.OptionFunc{
		pattern.WithC(true),
	}

	someOption := pattern.NewSomeOption("a", options...)
	fmt.Printf("%#v\n", someOption)
}
```

#### 1.2.2 默认值问题

通常我们在初始化实例这个场景下,对默认值的理解是:客户端提供了值,则使用客户端提供的;客户端没提供,则使用默认值.

在函数选项模式中,这种功能就非常容易提供了:

```go
package pattern

type SomeOption struct {
	A string
	B int
	C bool
}

const (
	// defaultValueB SomeOption 实例的 B 字段的默认值
	defaultValueB = 100
)

// NewSomeOption SomeOption 的构造函数
func NewSomeOption(a string, options ...OptionFunc) *SomeOption {
	someOption := &SomeOption{
		A: a,
		B: defaultValueB,
	}

	// 若客户端提供了修改B字段的选项函数 则在此处会覆盖掉默认值
	for _, option := range options {
		option(someOption)
	}

	return someOption
}
```

这样一来,就可以实现提供默认值的需求了.

### 1.3 接口类型的函数选项模式

在一些场景下,我们可能并不想对外暴露具体的配置结构体(也就是我们上述示例中的`SomeOption`),而是仅仅对外提供一个功能函数.这时我们会将对应的结构体定义为小写字母开头,将其限制只在包内部使用:

```go
package pattern

type someOption struct {
	a string
	b int
	c bool
}
```

#### 1.3.1 定义接口

```go
package pattern

// IOption 本接口用于定义函数选项行为
type IOption interface {
	// apply 本方法用于修改 someOption 结构体的字段值
	apply(someOption *someOption)
}
```

#### 1.3.2 定义实现

```go
package pattern

// funcOption 函数选项类型 因为要实现接口,所以不能再使用函数类型,只能使用结构体
type funcOption struct {
	// f 具体选项函数
	withFunc func(someOption *someOption)
}

func (o *funcOption) apply(someOption *someOption) {
	o.withFunc(someOption)
}
```

#### 1.3.3 定义函数选项类型的构造函数

```go
package pattern

// funcOption 函数选项类型 因为要实现接口,所以不能再使用函数类型,只能使用结构体
type funcOption struct {
	// f 具体选项函数
	withFunc func(someOption *someOption)
}

func (o *funcOption) apply(someOption *someOption) {
	o.withFunc(someOption)
}

// newFuncOption funcOption 的构造函数
func newFuncOption(withFunc func(someOption *someOption)) *funcOption {
	return &funcOption{withFunc: withFunc}
}
```

#### 1.3.4 定义各种`With()`函数

注意各种`With()`函数是要对外暴露的,因为客户端要通过这些`With()`函数来修改`someOption`实例的各属性值

```go
package pattern

// WithB 本函数用于修改 someOption 实例的 b 字段值
func WithB(b int) IOption {
	withFunc := func(someOption *someOption) {
		someOption.b = b
	}

	return &funcOption{withFunc: withFunc}
}

// WithC 本函数用于修改 someOption 实例的 c 字段值
func WithC(c bool) IOption {
	withFunc := func(someOption *someOption) {
		someOption.c = c
	}

	return &funcOption{withFunc: withFunc}
}
```

#### 1.3.5 实现对外提供的功能函数

这个功能函数的职责其实和1.2小节中客户端代码的职责是相同的,都是**实例化`someOption`**

```go
package pattern

import "fmt"

// NewSomeOption 本函数是 someOption 的构造函数
func NewSomeOption(a string, options ...IOption) {
	someOptionObj := &someOption{
		a: a,
	}

	for _, option := range options {
		option.apply(someOptionObj)
	}

	fmt.Printf("%#v\n", someOptionObj)
}
```

#### 1.3.6 客户端代码

```go
package main

import "optionWithInterface/pattern"

func main() {
	options := []pattern.IOption{
		pattern.WithB(10),
		pattern.WithC(true),
	}

	pattern.NewSomeOption("a", options...)
}
```

可以看到,使用函数选项模式时,若要实例化的对象上,字段发生了变化,仅需调整`options`即可.


# xiaochengxu


# 01.原力去水印

在 **微信小程序** 找了去水印关键字，进行了检索，发现有的不维护了

我部署的这个去水印没有广告，主要为了让那些想下载别人抖音视频（不想带有别人水印或者别人不让下载通过链接来下载）

图文的选择 **图集解析**

![](https://oss.laf.run/v8w6wa-keepass/images/qushuiyin_demo1.jpg)

![](https://oss.laf.run/v8w6wa-keepass/images/qushuiyin_demo2.jpg)

## 目前可以去除:

* 抖音
* 快手
* 小红书
* 西瓜视频

## 扫描来体验😄

![](https://oss.laf.run/v8w6wa-keepass/images/yuanliqushuiyin.jpeg)


# 02.KeePass密码管理：安全轻松的管理您的密码

【KeePass密码管理】是一款简单、安全简洁的账号密码管理工具，支持指纹验证、faceId，N重安全保障，可以随时随地记录我的账号和密码。

写这个小程序之前，在国内市场找了很多密码存储类的APP体验不是太好用。之前还遇到数据全部丢失的app...无奈之下，想着自己搞了一个，而且是小程序，很方便来使用。

密码加密规则（三重保障）：

* 主密码：首次进入小程序添加密码，需要设置主密码，主密码使用SHA-256摘要算法存储于服务器（不可逆，单向的，只有自己知道，别人无法破解）。
* 服务端账号密码存储使用AES对称加密方式进行加密，主密码为加密密码，请勿遗忘主密码，遗忘了无法解密（基于单向加密的主密码做双向加密）。
* 数据均存储至小程序云开发数据库，数据安全保障，不丢不盗!

![](https://oss.laf.run/v8w6wa-keepass/images/mimaguanli.jpeg)

【KeePass密码管理】主要功能如下：

* 设置一个主密码，用过主密码加密解密账号数据
* 支持指纹/人脸识别验证读取密码

## 首页比较简洁，主要提供搜索、添加密码、查看所有密码

![](https://oss.laf.run/v8w6wa-keepass/images/password_demo1.png)

### 记个密码: 提供最常用的进行展示，主要包含

* 描述
* 用户名（支持个性化，可以自定义用户名）
* 密码（可以根据自己的生成规则一键生成密码并且复制好）

![](https://oss.laf.run/v8w6wa-keepass/images/password_demo2.png)

### 更多选项支持一些常用的选填项

* 链接/平台
* 手机
* 邮箱
* 备注

![](https://oss.laf.run/v8w6wa-keepass/images/password_demo3.png)

## 全部记录

1.人脸识别/指纹识别 2.每次查看都会更新最新的查看时间 3.支持修改和删除

![](https://oss.laf.run/v8w6wa-keepass/images/password_demo4.png)

![](https://oss.laf.run/v8w6wa-keepass/images/password_demo5.png)

另外在偏好设置提供了【清空账号数据】，用户随时可以从库中把自己账号和密码全部清空。

## 扫描来体验😄

![](https://oss.laf.run/v8w6wa-keepass/images/mimaguanli.jpeg)


