|
|
# 04|中间件:如何提高框架的可拓展性?
|
|
|
|
|
|
你好,我是轩脉刃。
|
|
|
|
|
|
到目前为止我们已经完成了Web框架的基础部分,使用net/http启动了一个Web服务,并且定义了自己的Context,可以控制请求超时。
|
|
|
|
|
|
之前在讲具体实现的时候,我们反复强调要注意代码的优化。那么如何优化呢?具体来说,很重要的一点就是封装。所以今天我们就回顾一下之前写的代码,看看如何通过封装来进一步提高代码扩展性。
|
|
|
|
|
|
在第二课,我们在业务文件夹中的controller.go的逻辑中设置了一个有超时时长的控制器:
|
|
|
|
|
|
```go
|
|
|
func FooControllerHandler(c *framework.Context) error {
|
|
|
...
|
|
|
// 在业务逻辑处理前,创建有定时器功能的 context
|
|
|
durationCtx, cancel := context.WithTimeout(c.BaseContext(), time.Duration(1*time.Second))
|
|
|
defer cancel()
|
|
|
|
|
|
go func() {
|
|
|
...
|
|
|
// 执行具体的业务逻辑
|
|
|
|
|
|
time.Sleep(10 * time.Second)
|
|
|
// ...
|
|
|
|
|
|
finish <- struct{}{}
|
|
|
}()
|
|
|
// 在业务逻辑处理后,操作输出逻辑...
|
|
|
select {
|
|
|
...
|
|
|
case <-finish:
|
|
|
fmt.Println("finish")
|
|
|
...
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
在正式执行业务逻辑之前,创建了一个具有定时器功能的 Context,然后开启一个 Goroutine 执行正式的业务逻辑,并且监听定时器和业务逻辑,哪个先完成,就先输出内容。
|
|
|
|
|
|
首先从代码功能分析,这个控制器像由两部分组成。
|
|
|
|
|
|
一部分是**业务逻辑**,也就是time.Sleep函数所代表的逻辑,在实际生产过程中,这里会有很重的业务逻辑代码;而另一部分是**非业务逻辑**,比如创建Context、通道等待finish信号等。很明显,这个非业务逻辑是非常通用的需求,可能在多个控制器中都会使用到。
|
|
|
|
|
|
而且考虑复用性,这里只是写了一个控制器,那如果有多个控制器呢,我们难道要为每个控制器都写上这么一段超时代码吗?那就非常冗余了。
|
|
|
|
|
|
所以,能不能设计一个机制,**将这些非业务逻辑代码抽象出来,封装好,提供接口给控制器使用**。这个机制的实现,就是我们今天要讲的中间件。
|
|
|
|
|
|
怎么实现这个中间件呢?我们再观察一下刚才的代码找找思路。
|
|
|
|
|
|
代码的组织顺序很清晰,先预处理请求,再处理业务逻辑,最后处理返回值,你发现没有这种顺序,其实很符合设计模式中的装饰器模式。装饰器模式,顾名思义,就是在核心处理模块的外层增加一个又一个的装饰,类似洋葱。![](https://static001.geekbang.org/resource/image/f9/2c/f94ccc78af2ca491afe1591e674e3f2c.jpg?wh=1920x912)
|
|
|
|
|
|
现在,抽象出中间件的思路是不是就很清晰了,把核心业务逻辑先封装起来,然后一层一层添加装饰,最终让所有请求正序一层层通过装饰器,进入核心处理模块,再反序退出装饰器。原理就是这么简单,不难理解,我们接着看该如何实现。
|
|
|
|
|
|
## 使用函数嵌套方式实现中间件
|
|
|
|
|
|
装饰器模式是一层一层的,所以具体实现其实也不难想到,就是使用函数嵌套。
|
|
|
|
|
|
首先,我们封装核心的业务逻辑。就是说,这个中间件的输入是一个核心的业务逻辑 ControllerHandler,输出也应该是一个 ControllerHandler。所以**对于一个超时控制器,我们可以定义一个中间件为 TimeoutHandler**。
|
|
|
|
|
|
在框架文件夹中,我们创建一个timeout.go文件来存放这个中间件。
|
|
|
|
|
|
```go
|
|
|
func TimeoutHandler(fun ControllerHandler, d time.Duration) ControllerHandler {
|
|
|
// 使用函数回调
|
|
|
return func(c *Context) error {
|
|
|
|
|
|
finish := make(chan struct{}, 1)
|
|
|
panicChan := make(chan interface{}, 1)
|
|
|
|
|
|
// 执行业务逻辑前预操作:初始化超时 context
|
|
|
durationCtx, cancel := context.WithTimeout(c.BaseContext(), d)
|
|
|
defer cancel()
|
|
|
|
|
|
c.request.WithContext(durationCtx)
|
|
|
|
|
|
go func() {
|
|
|
defer func() {
|
|
|
if p := recover(); p != nil {
|
|
|
panicChan <- p
|
|
|
}
|
|
|
}()
|
|
|
// 执行具体的业务逻辑
|
|
|
fun(c)
|
|
|
|
|
|
finish <- struct{}{}
|
|
|
}()
|
|
|
// 执行业务逻辑后操作
|
|
|
select {
|
|
|
case p := <-panicChan:
|
|
|
log.Println(p)
|
|
|
c.responseWriter.WriteHeader(500)
|
|
|
case <-finish:
|
|
|
fmt.Println("finish")
|
|
|
case <-durationCtx.Done():
|
|
|
c.SetHasTimeout()
|
|
|
c.responseWriter.Write([]byte("time out"))
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
仔细看下这段代码,中间件函数的返回值是一个匿名函数,这个匿名函数实现了ControllerHandler 函数结构,参数为Context,返回值为error。
|
|
|
|
|
|
在这个匿名函数中,我们先创建了一个定时器Context,然后开启一个Goroutine,在Goroutine中执行具体的业务逻辑。这个Goroutine会在业务逻辑执行结束后,通过一个finish的channel来传递结束信号;也会在业务出现异常的时候,通过panicChan来传递异常信号。
|
|
|
|
|
|
而在业务逻辑之外的主Goroutine中,会同时进行多个信号的监听操作,包括结束信号、异常信号、超时信号,耗时最短的信号到达后,请求结束。这样,我们就完成了设置业务超时的任务。
|
|
|
|
|
|
于是在业务文件夹route.go中,路由注册就可以修改为:
|
|
|
|
|
|
```go
|
|
|
// 在核心业务逻辑 UserLoginController 之外,封装一层 TimeoutHandler
|
|
|
core.Get("/user/login", framework.TimeoutHandler(UserLoginController, time.Second))
|
|
|
|
|
|
```
|
|
|
|
|
|
这种函数嵌套方式,让下层中间件是上层中间件的参数,通过一层层嵌套实现了中间件的装饰器模式。
|
|
|
|
|
|
但是你再想一步,就会发现,这样实现的中间件机制有两个问题:
|
|
|
|
|
|
1. **中间件是循环嵌套的**,当有多个中间件的时候,整个嵌套长度就会非常长,非常不优雅的,比如:
|
|
|
|
|
|
```go
|
|
|
TimeoutHandler(LogHandler(recoveryHandler(UserLoginController)))
|
|
|
|
|
|
```
|
|
|
|
|
|
2. 刚才的实现,**只能为单个业务控制器设置中间件,不能批量设置**。上一课我们开发的路由是具有同前缀分组功能的(IGroup),需要批量为某个分组设置一个超时时长。
|
|
|
|
|
|
所以,我们要对刚才实现的简单中间件代码做一些改进。怎么做呢?
|
|
|
|
|
|
## 使用 pipeline 思想改造中间件
|
|
|
|
|
|
一层层嵌套不好用,如果我们将每个核心控制器所需要的中间件,使用一个数组链接(Chain)起来,形成一条流水线(Pipeline),就能完美解决这两个问题了。
|
|
|
|
|
|
请求流的流向如下图所示:![](https://static001.geekbang.org/resource/image/e1/2a/e1aa5937627e46c8b2b21e45426f342a.jpg?wh=1920x915)
|
|
|
|
|
|
这个Pipeline模型和前面的洋葱模型不一样的点在于,**Middleware不再以下一层的ControllerHandler为参数了,它只需要返回有自身中间件逻辑的ControllerHandler**。
|
|
|
|
|
|
也就是在框架文件夹中的timeout.go中,我们将Middleware的形式从刚才的:
|
|
|
|
|
|
```go
|
|
|
func TimeoutHandler(fun ControllerHandler, d time.Duration) ControllerHandler {
|
|
|
// 使用函数回调
|
|
|
return func(c *Context) error {
|
|
|
//...
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
变成这样:
|
|
|
|
|
|
```go
|
|
|
// 超时控制器参数中ControllerHandler结构已经去掉
|
|
|
func Timeout(d time.Duration) framework.ControllerHandler {
|
|
|
// 使用函数回调
|
|
|
return func(c *framework.Context) error {
|
|
|
//...
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
但是在中间件注册的回调函数中,如何调用下一个ControllerHandler呢?在回调函数中,只有framework.Context 这个数据结构作为参数。
|
|
|
|
|
|
所以就需要我们在Context这个数据结构中想一些办法了。回顾下目前有的数据结构:Core、Context、Tree、Node、Group。![](https://static001.geekbang.org/resource/image/8e/7b/8ef582e74e9c5ca1c0f54e7c1d75a67b.jpg?wh=1920x1277)
|
|
|
|
|
|
它们基本上都是以 Core 为中心,在 Core 中设置路由 router,实现了 Tree 结构,在 Tree 结构中包含路由节点 node;在注册路由的时候,将对应的业务核心处理逻辑 handler ,放在 node 结构的 handler 属性中。
|
|
|
|
|
|
而 Core 中的 ServeHttp 方法会创建 Context 数据结构,然后ServeHttp方法再根据 Request-URI 查找指定 node,并且将 Context 结构和 node 中的控制器 ControllerHandler 结合起来执行具体的业务逻辑。
|
|
|
|
|
|
结构都梳理清楚了,怎么改造成流水线呢?
|
|
|
|
|
|
我们可以**将每个中间件构造出来的 ControllerHandler 和最终的业务逻辑的 ControllerHandler 结合在一起**,成为一个 ControllerHandler 数组,也就是控制器链。在最终执行业务代码的时候,能一个个调用控制器链路上的控制器。
|
|
|
|
|
|
这个想法其实是非常自然的,因为中间件中创造出来的ControllerHandler匿名函数,和最终的控制器业务逻辑ControllerHandler,都是**同样的结构**,**所以我们可以选用Controllerhander的数组,来表示某个路由的业务逻辑**。
|
|
|
|
|
|
对应到代码上,我们先搞清楚使用链路的方式,再看如何注册和构造链路。
|
|
|
|
|
|
### 如何使用控制器链路
|
|
|
|
|
|
首先,我们研究下如何使用这个控制器链路,即图中右边部分的改造。
|
|
|
![](https://static001.geekbang.org/resource/image/49/c8/49c2d50b26d48e338c3acd2e1374f4c8.jpg?wh=1920x1277)
|
|
|
|
|
|
第一步,我们需要修改路由节点node。
|
|
|
|
|
|
在node节点中将原先的Handler,替换为控制器链路Handlers。这样在寻找路由节点的时候,就能找到对应的控制器链路了。修改框架文件夹中存放trie树的trie.go文件:
|
|
|
|
|
|
```go
|
|
|
// 代表节点
|
|
|
type node struct {
|
|
|
...
|
|
|
handlers []ControllerHandler // 中间件+控制器
|
|
|
...
|
|
|
}
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
第二步,我们修改Context结构。
|
|
|
|
|
|
由于我们上文提到,在中间件注册的回调函数中,只有framework.Context 这个数据结构作为参数,所以在Context中也需要保存这个控制器链路(handlers),并且要记录下当前执行到了哪个控制器(index)。修改框架文件夹的context.go文件:
|
|
|
|
|
|
```go
|
|
|
// Context代表当前请求上下文
|
|
|
type Context struct {
|
|
|
...
|
|
|
|
|
|
// 当前请求的handler链条
|
|
|
handlers []ControllerHandler
|
|
|
index int // 当前请求调用到调用链的哪个节点
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
第三步,来实现链条调用方式。
|
|
|
|
|
|
为了控制实现链条的逐步调用,我们为Context实现一个Next方法。这个Next方法每调用一次,就将这个控制器链路的调用控制器,往后移动一步。继续在框架文件夹中的context.go文件里写:
|
|
|
|
|
|
```go
|
|
|
// 核心函数,调用context的下一个函数
|
|
|
func (ctx *Context) Next() error {
|
|
|
ctx.index++
|
|
|
if ctx.index < len(ctx.handlers) {
|
|
|
if err := ctx.handlers[ctx.index](ctx); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
这里我再啰嗦一下,Next() 函数是整个链路执行的重点,要好好理解,它通过维护Context中的一个下标,来控制链路移动,这个下标表示当前调用Next要执行的控制器序列。
|
|
|
|
|
|
Next() 函数会在框架的两个地方被调用:
|
|
|
|
|
|
* 第一个是在此次请求处理的入口处,即Core的ServeHttp;
|
|
|
* 第二个是在每个中间件的逻辑代码中,用于调用下个中间件。
|
|
|
![](https://static001.geekbang.org/resource/image/73/3c/73a80752cf6d94b90febd2e23e80bc3c.jpg?wh=1920x915)
|
|
|
|
|
|
这里要注意,index下标表示当前调用Next要执行的控制器序列,它的**初始值应该为-1,每次调用都会自增1**,这样才能保证第一次调用的时候index为0,定位到控制器链条的下标为0的控制器,即第一个控制器。
|
|
|
|
|
|
在框架文件夹context.go的初始化Context函数中,代码如下:
|
|
|
|
|
|
```go
|
|
|
// NewContext 初始化一个Context
|
|
|
func NewContext(r *http.Request, w http.ResponseWriter) *Context {
|
|
|
return &Context{
|
|
|
...
|
|
|
index: -1,
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
被调用的第一个地方,在入口处调用的代码,写在框架文件夹中的core.go文件中:
|
|
|
|
|
|
```go
|
|
|
// 所有请求都进入这个函数, 这个函数负责路由分发
|
|
|
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
|
|
|
|
|
|
// 封装自定义context
|
|
|
ctx := NewContext(request, response)
|
|
|
|
|
|
// 寻找路由
|
|
|
handlers := c.FindRouteByRequest(request)
|
|
|
if handlers == nil {
|
|
|
// 如果没有找到,这里打印日志
|
|
|
ctx.Json(404, "not found")
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 设置context中的handlers字段
|
|
|
ctx.SetHandlers(handlers)
|
|
|
|
|
|
// 调用路由函数,如果返回err 代表存在内部错误,返回500状态码
|
|
|
if err := ctx.Next(); err != nil {
|
|
|
ctx.Json(500, "inner error")
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
被调用的第二个位置在中间件中,每个中间件都通过调用 context.Next 来调用下一个中间件。所以我们可以在框架文件夹中创建middleware目录,其中创建一个test.go存放我们的测试中间件:
|
|
|
|
|
|
```go
|
|
|
func Test1() framework.ControllerHandler {
|
|
|
// 使用函数回调
|
|
|
return func(c *framework.Context) error {
|
|
|
fmt.Println("middleware pre test1")
|
|
|
c.Next() // 调用Next往下调用,会自增contxt.index
|
|
|
fmt.Println("middleware post test1")
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func Test2() framework.ControllerHandler {
|
|
|
// 使用函数回调
|
|
|
return func(c *framework.Context) error {
|
|
|
fmt.Println("middleware pre test2")
|
|
|
c.Next() // 调用Next往下调用,会自增contxt.index
|
|
|
fmt.Println("middleware post test2")
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
### 如何注册控制器链路
|
|
|
|
|
|
如何使用控制器链路,我们就讲完了,再看控制器链路如何注册,就是之前UML图的左边部分。![](https://static001.geekbang.org/resource/image/3c/b5/3c2012fcfcabcfc0159e4ecec2fdb8b5.jpg?wh=1920x1277)
|
|
|
|
|
|
很明显,现有的函数没有包含注册中间件逻辑,所以我们需要为Group和Core两个结构增加注册中间件入口,要设计两个地方:
|
|
|
|
|
|
* Core和Group单独设计一个Use函数,为其数据结构负责的路由批量设置中间件
|
|
|
* 为Core和Group注册单个路由的 Get / Post / Put / Delete 函数,设置中间件
|
|
|
|
|
|
先看下批量设置中间件的Use函数,我们在框架文件夹中的core.go修改:
|
|
|
|
|
|
```go
|
|
|
// 注册中间件
|
|
|
func (c *Core) Use(middlewares ...ControllerHandler) {
|
|
|
c.middlewares = append(c.middlewares, middlewares...)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
和框架文件夹中的group.go中修改:
|
|
|
|
|
|
```go
|
|
|
// 注册中间件
|
|
|
func (g *Group) Use(middlewares ...ControllerHandler) {
|
|
|
g.middlewares = append(g.middlewares, middlewares...)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
注意下这里的参数,使用的是Golang的可变参数,**这个可变参数代表,我可以传递0~n个ControllerHandler类型的参数**,这个设计会增加函数的易用性。它在业务文件夹中使用起来的形式是这样的,在main.go中:
|
|
|
|
|
|
```go
|
|
|
// core中使用use注册中间件
|
|
|
core.Use(
|
|
|
middleware.Test1(),
|
|
|
middleware.Test2())
|
|
|
|
|
|
// group中使用use注册中间件
|
|
|
subjectApi := core.Group("/subject")
|
|
|
subjectApi.Use(middleware.Test3())
|
|
|
|
|
|
```
|
|
|
|
|
|
再看单个路由设置中间件的函数,我们也使用可变参数,改造注册路由的函数(Get /Post /Delete /Put),继续在框架文件夹中的core.go里修改:
|
|
|
|
|
|
```go
|
|
|
// Core的Get方法进行改造
|
|
|
func (c *Core) Get(url string, handlers ...ControllerHandler) {
|
|
|
// 将core的middleware 和 handlers结合起来
|
|
|
allHandlers := append(c.middlewares, handlers...)
|
|
|
if err := c.router["GET"].AddRouter(url, allHandlers); err != nil {
|
|
|
log.Fatal("add router error: ", err)
|
|
|
}
|
|
|
}
|
|
|
...
|
|
|
|
|
|
```
|
|
|
|
|
|
同时修改框架文件夹中的group.go:
|
|
|
|
|
|
```go
|
|
|
// 改造IGroup 的所有方法
|
|
|
type IGroup interface {
|
|
|
// 实现HttpMethod方法
|
|
|
Get(string, ...ControllerHandler)
|
|
|
Post(string, ...ControllerHandler)
|
|
|
Put(string, ...ControllerHandler)
|
|
|
Delete(string, ...ControllerHandler)
|
|
|
//..
|
|
|
}
|
|
|
|
|
|
// 改造Group的Get方法
|
|
|
func (g *Group) Get(uri string, handlers ...ControllerHandler) {
|
|
|
uri = g.getAbsolutePrefix() + uri
|
|
|
allHandlers := append(g.getMiddlewares(), handlers...)
|
|
|
g.core.Get(uri, allHandlers...)
|
|
|
}
|
|
|
|
|
|
...
|
|
|
|
|
|
```
|
|
|
|
|
|
这样,回到业务文件夹中的router.go,我们注册路由的使用方法就可以变成如下形式:
|
|
|
|
|
|
```go
|
|
|
// 注册路由规则
|
|
|
func registerRouter(core *framework.Core) {
|
|
|
// 在core中使用middleware.Test3() 为单个路由增加中间件
|
|
|
core.Get("/user/login", middleware.Test3(), UserLoginController)
|
|
|
|
|
|
// 批量通用前缀
|
|
|
subjectApi := core.Group("/subject")
|
|
|
{
|
|
|
...
|
|
|
// 在group中使用middleware.Test3() 为单个路由增加中间件
|
|
|
subjectApi.Get("/:id", middleware.Test3(), SubjectGetController)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
不管是通过批量注册中间件,还是单个注册中间件,最终都要汇总到路由节点node中,所以这里我们调用了上一节课最终增加路由的函数Tree.AddRouter,把将这个请求对应的Core结构里的中间件和Group结构里的中间件,都聚合起来,成为最终路由节点的中间件。
|
|
|
|
|
|
聚合的逻辑在group.go和core.go中都有,实际上就是**将Handler和Middleware一起放在一个数组中**。
|
|
|
|
|
|
```go
|
|
|
// 获取某个group的middleware
|
|
|
// 这里就是获取除了Get/Post/Put/Delete之外设置的middleware
|
|
|
func (g *Group) getMiddlewares() []ControllerHandler {
|
|
|
if g.parent == nil {
|
|
|
return g.middlewares
|
|
|
}
|
|
|
|
|
|
return append(g.parent.getMiddlewares(), g.middlewares...)
|
|
|
}
|
|
|
|
|
|
// 实现Get方法
|
|
|
func (g *Group) Get(uri string, handlers ...ControllerHandler) {
|
|
|
uri = g.getAbsolutePrefix() + uri
|
|
|
allHandlers := append(g.getMiddlewares(), handlers...)
|
|
|
g.core.Get(uri, allHandlers...)
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
在core.go文件夹里写:
|
|
|
|
|
|
```go
|
|
|
// 匹配GET 方法, 增加路由规则
|
|
|
func (c *Core) Get(url string, handlers ...ControllerHandler) {
|
|
|
// 将core的middleware 和 handlers结合起来
|
|
|
allHandlers := append(c.middlewares, handlers...)
|
|
|
if err := c.router["GET"].AddRouter(url, allHandlers); err != nil {
|
|
|
log.Fatal("add router error: ", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
到这里,我们使用 pipeline 思想对中间件的改造就完成了, 最终的UML类图如下:
|
|
|
![](https://static001.geekbang.org/resource/image/7f/ab/7f26e60d79ec987dba10a1b5045aa2ab.jpg?wh=1920x1277)
|
|
|
|
|
|
让我们简要回顾下改造过程。
|
|
|
|
|
|
第一步使用控制器链路,我们**改造了node和Context两个数据结构**。为node增加了handlers,存放这个路由注册的所有中间件;Context也增加了handlers,在Core.ServeHttp的函数中,创建Context结构,寻找到请求对应的路由节点,然后把路由节点的handlers数组,复制到Context中的handlers。
|
|
|
|
|
|
为了实现真正的链路调用,需要在框架的**两个地方调用Context.Next() 方法**,一个是启动业务逻辑的地方,一个是每个中间件的调用。
|
|
|
|
|
|
第二步如何注册控制器链路,我们**改造了Group和Core两个数据结构,为它们增加了注册中间件的入口**,一处是批量增加中间件函数Use,一处是在注册单个路由的Get / Post / Delete / Put方法中,为单个路由设置中间件。在设计入口的时候,我们使用了可变参数的设计,提高注册入口的可用性。
|
|
|
|
|
|
## 基本的中间件: Recovery
|
|
|
|
|
|
我们现在已经将中间件机制搭建并运行起来了, 但是具体需要实现哪些中间件呢?这要根据不同需求进行不同的研发,是个长期话题。
|
|
|
|
|
|
这里我们演示一个最基本的中间件:Recovery。
|
|
|
|
|
|
中间件那么多,比如超时中间件、统计中间件、日志中间件,为什么我说Recovery是最基本的呢?给出我的想法之前,你可以先思考这个问题:在编写业务核心逻辑的时候,如果出现了一个panic,而且在业务核心逻辑函数中未捕获处理,会发生什么?
|
|
|
|
|
|
我们还是基于第一节课讲的net/http的主流程逻辑来思考,关键结论有一点是,**每个HTTP连接都会开启一个Goroutine为其服务**,所以很明显, net/http 的进程模型是单进程、多协程。![](https://static001.geekbang.org/resource/image/0f/ee/0fa86b64b6d1b1e96560420243ec6aee.jpg?wh=1920x1133)
|
|
|
|
|
|
在Golang的这种模型中,每个协程是独立且平等的,即使是创建子协程的父协程,在Goroutine 中也无法管理子协程。所以,**每个协程需要自己保证不会外抛panic**,一旦外抛panic了,整个进程就认为出现异常,会终止进程。
|
|
|
|
|
|
这一点搞清楚了,再看Recovery为什么必备就很简单。在net/http处理业务逻辑的协程中,要捕获在自己这个协程中抛出的panic,就必须自己实现 Recovery 机制。
|
|
|
|
|
|
而Recovery中间件就是用来为每个协程增加Recovery机制的。我们在框架的middleware文件夹中增加recovery.go存放这个中间件:
|
|
|
|
|
|
```go
|
|
|
// recovery机制,将协程中的函数异常进行捕获
|
|
|
func Recovery() framework.ControllerHandler {
|
|
|
// 使用函数回调
|
|
|
return func(c *framework.Context) error {
|
|
|
// 核心在增加这个recover机制,捕获c.Next()出现的panic
|
|
|
defer func() {
|
|
|
if err := recover(); err != nil {
|
|
|
c.Json(500, err)
|
|
|
}
|
|
|
}()
|
|
|
// 使用next执行具体的业务逻辑
|
|
|
c.Next()
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
这个中间件就是在context.Next() 之前设置了defer 函数,这个函数的作用就是捕获c.Next()中抛出的异常panic。之后在业务文件夹中的main.go,我们就可以通过Core结构的Use方法,对所有的路由都设置这个中间件。
|
|
|
|
|
|
```go
|
|
|
core.Use(middleware.Recovery())
|
|
|
|
|
|
```
|
|
|
|
|
|
今天所有代码的目录结构截图,我也贴在这里供你对比检查,代码放在GitHub上的 [04分支](https://github.com/gohade/coredemo/tree/geekbang/04/framework)里。
|
|
|
![](https://static001.geekbang.org/resource/image/b8/yy/b8a053e4650ec9560754383d0f3974yy.png?wh=784x1274)
|
|
|
|
|
|
## 小结
|
|
|
|
|
|
今天我们最终为自己的框架增加了中间件机制。中间件机制的本质就是装饰器模型,对核心的逻辑函数进行装饰、封装,所以一开始我们就使用函数嵌套的方式实现了中间件机制。
|
|
|
|
|
|
但是实现之后,我们发现函数嵌套的弊端:一是不优雅,二是无法批量设置中间件。所以我们**引入了pipeline的思想,将所有中间件做成一个链条,通过这个链条的调用,来实现中间件机制**。
|
|
|
|
|
|
最后,我们选了最基础的Recovery中间件演示如何具体实现,一方面作为中间件机制的示例,另一方面,也在功能上为我们的框架增强了健壮性。
|
|
|
|
|
|
中间件机制是我们必须要掌握的机制,很多Web框架中都有这个逻辑。**在架构层面,中间件机制就相当于,在每个请求的横切面统一注入了一个逻辑**。这种统一处理的逻辑是非常有用的,比如统一打印日志、统一打点到统计系统、统一做权限登录验证等。
|
|
|
|
|
|
## 思考题
|
|
|
|
|
|
现在希望能对每个请求都进行请求时长统计,所以想写一个请求时长统计的中间件,在日志中输出请求 URI、请求耗时。不知道你如何实现呢?
|
|
|
|
|
|
欢迎在留言区分享你的思考。如果你觉得今天的内容对你有所帮助,也欢迎分享给你身边的朋友,邀请他一起学习~
|
|
|
|