04|中间件:如何提高框架的可拓展性?

思考并回答以下问题:

framework/context.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
package framework

import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"strconv"
"sync"
"time"
)

// Context代表当前请求上下文
type Context struct {
request *http.Request
responseWriter http.ResponseWriter
ctx context.Context

// 是否超时标记位
hasTimeout bool
writerMux *sync.Mutex

// 当前请求的handler链条
handlers []ControllerHandler
index int // 当前请求调用到调用链的哪个节点
}

// NewContext 初始化一个Context
func NewContext(r *http.Request, w http.ResponseWriter) *Context {
return &Context{
request: r,
responseWriter: w,
ctx: r.Context(),
writerMux: &sync.Mutex{},
index: -1,
}
}

// #region base function

func (ctx *Context) WriterMux() *sync.Mutex {
return ctx.writerMux
}

func (ctx *Context) GetRequest() *http.Request {
return ctx.request
}

func (ctx *Context) GetResponse() http.ResponseWriter {
return ctx.responseWriter
}

func (ctx *Context) SetHasTimeout() {
ctx.hasTimeout = true
}

func (ctx *Context) HasTimeout() bool {
return ctx.hasTimeout
}

// 为context设置handlers
func (ctx *Context) SetHandlers(handlers []ControllerHandler) {
ctx.handlers = handlers
}

// 核心函数,调用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
}

// #endregion

func (ctx *Context) BaseContext() context.Context {
return ctx.request.Context()
}

// #region implement context.Context
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
return ctx.BaseContext().Deadline()
}

func (ctx *Context) Done() <-chan struct{} {
return ctx.BaseContext().Done()
}

func (ctx *Context) Err() error {
return ctx.BaseContext().Err()
}

func (ctx *Context) Value(key interface{}) interface{} {
return ctx.BaseContext().Value(key)
}

// #endregion

// #region query url
func (ctx *Context) QueryInt(key string, def int) int {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
intval, err := strconv.Atoi(vals[len-1])
if err != nil {
return def
}
return intval
}
}
return def
}

func (ctx *Context) QueryString(key string, def string) string {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
return vals[len-1]
}
}
return def
}

func (ctx *Context) QueryArray(key string, def []string) []string {
params := ctx.QueryAll()
if vals, ok := params[key]; ok {
return vals
}
return def
}

func (ctx *Context) QueryAll() map[string][]string {
if ctx.request != nil {
return map[string][]string(ctx.request.URL.Query())
}
return map[string][]string{}
}

// #endregion

// #region form post
func (ctx *Context) FormInt(key string, def int) int {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
intval, err := strconv.Atoi(vals[len-1])
if err != nil {
return def
}
return intval
}
}
return def
}

func (ctx *Context) FormString(key string, def string) string {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
len := len(vals)
if len > 0 {
return vals[len-1]
}
}
return def
}

func (ctx *Context) FormArray(key string, def []string) []string {
params := ctx.FormAll()
if vals, ok := params[key]; ok {
return vals
}
return def
}

func (ctx *Context) FormAll() map[string][]string {
if ctx.request != nil {
return map[string][]string(ctx.request.PostForm)
}
return map[string][]string{}
}

// #endregion

// #region application/json post

func (ctx *Context) BindJson(obj interface{}) error {
if ctx.request != nil {
body, err := ioutil.ReadAll(ctx.request.Body)
if err != nil {
return err
}
ctx.request.Body = ioutil.NopCloser(bytes.NewBuffer(body))

err = json.Unmarshal(body, obj)
if err != nil {
return err
}
} else {
return errors.New("ctx.request empty")
}
return nil
}

// #endregion

// #region response

func (ctx *Context) Json(status int, obj interface{}) error {
if ctx.HasTimeout() {
return nil
}
ctx.responseWriter.Header().Set("Content-Type", "application/json")
ctx.responseWriter.WriteHeader(status)
byt, err := json.Marshal(obj)
if err != nil {
ctx.responseWriter.WriteHeader(500)
return err
}
ctx.responseWriter.Write(byt)
return nil
}

func (ctx *Context) HTML(status int, obj interface{}, template string) error {
return nil
}

func (ctx *Context) Text(status int, obj string) error {
return nil
}

// #endregion

framework/middleware/recovery.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package middleware

import (
"coredemo/framework"
)

// 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
}
}

之前在讲具体实现的时候,我们反复强调要注意代码的优化。那么如何优化呢?具体来说,很重要的一点就是封装。所以今天我们就回顾一下之前写的代码,看看如何通过封装来进一步提高代码扩展性。

在第二课,我们在业务文件夹中的controller.go的逻辑中设置了一个有超时时长的控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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信号等。很明显,这个非业务逻辑是非常通用的需求,可能在多个控制器中都会使用到。

而且考虑复用性,这里只是写了一个控制器,那如果有多个控制器呢,我们难道要为每个控制器都写上这么一段超时代码吗?那就非常冗余了。

所以,能不能设计一个机制,将这些非业务逻辑代码抽象出来,封装好,提供接口给控制器使用。这个机制的实现,就是我们今天要讲的中间件。

怎么实现这个中间件呢?我们再观察一下刚才的代码找找思路。

代码的组织顺序很清晰,先预处理请求,再处理业务逻辑,最后处理返回值,你发现没有这种顺序,其实很符合设计模式中的装饰器模式。装饰器模式,顾名思义,就是在核心处理模块的外层增加一个又一个的装饰,类似洋葱。

现在,抽象出中间件的思路是不是就很清晰了,把核心业务逻辑先封装起来,然后一层一层添加装饰,最终让所有请求正序一层层通过装饰器,进入核心处理模块,再反序退出装饰器。原理就是这么简单,不难理解,我们接着看该如何实现。

中间件

使用函数嵌套方式实现中间件

装饰器模式是一层一层的,所以具体实现其实也不难想到,就是使用函数嵌套。

首先,我们封装核心的业务逻辑。就是说,这个中间件的输入是一个核心的业务逻辑ControllerHandler,输出也应该是一个ControllerHandler。所以对于一个超时控制器,我们可以定义一个中间件为TimeoutHandler

在框架文件夹中,我们创建一个timeout.go文件来存放这个中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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中,路由注册就可以修改为:

1
2
// 在核心业务逻辑 UserLoginController 之外,封装一层 TimeoutHandler
core.Get("/user/login", framework.TimeoutHandler(UserLoginController, time.Second))

这种函数嵌套方式,让下层中间件是上层中间件的参数,通过一层层嵌套实现了中间件的装饰器模式。

但是你再想一步,就会发现,这样实现的中间件机制有两个问题:

1,中间件是循环嵌套的,当有多个中间件的时候,整个嵌套长度就会非常长,非常不优雅的,比如:

1
TimeoutHandler(LogHandler(recoveryHandler(UserLoginController)))

2,刚才的实现,只能为单个业务控制器设置中间件,不能批量设置。上一课我们开发的路由是具有同前缀分组功能的(IGroup),需要批量为某个分组设置一个超时时长。

所以,我们要对刚才实现的简单中间件代码做一些改进。怎么做呢?

使用pipeline思想改造中间件

一层层嵌套不好用,如果我们将每个核心控制器所需要的中间件,使用一个数组链接(Chain)起来,形成一条流水线(Pipeline),就能完美解决这两个问题了。

请求流的流向如下图所示:

这个Pipeline模型和前面的洋葱模型不一样的点在于,Middleware不再以下一层的ControllerHandler为参数了,它只需要返回有自身中间件逻辑的ControllerHandler

也就是在框架文件夹中的timeout.go中,我们将Middleware的形式从刚才的:

1
2
3
4
5
6
func TimeoutHandler(fun ControllerHandler, d time.Duration) ControllerHandler {
// 使用函数回调
return func(c *Context) error {
//...
}
}

变成这样:
1
2
3
4
5
6
7
// 超时控制器参数中ControllerHandler结构已经去掉
func Timeout(d time.Duration) framework.ControllerHandler {
// 使用函数回调
return func(c *framework.Context) error {
//...
}
}

但是在中间件注册的回调函数中,如何调用下一个ControllerHandler呢?在回调函数中,只有framework.Context这个数据结构作为参数。

所以就需要我们在Context这个数据结构中想一些办法了。回顾下目前有的数据结构:Core、Context、Tree、Node、Group。

它们基本上都是以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的数组,来表示某个路由的业务逻辑

对应到代码上,我们先搞清楚使用链路的方式,再看如何注册和构造链路。

如何使用控制器链路

首先,我们研究下如何使用这个控制器链路,即图中右边部分的改造。

第一步,我们需要修改路由节点node。

在node节点中将原先的Handler,替换为控制器链路Handlers。这样在寻找路由节点的时候,就能找到对应的控制器链路了。修改框架文件夹中存放trie树的trie.go文件:

1
2
3
4
5
6
// 代表节点
type node struct {
...
handlers []ControllerHandler // 中间件+控制器
...
}

第二步,我们修改Context结构。

由于我们上文提到,在中间件注册的回调函数中,只有framework.Context这个数据结构作为参数,所以在Context中也需要保存这个控制器链路(handlers),并且要记录下当前执行到了哪个控制器(index)。修改框架文件夹的context.go文件:

1
2
3
4
5
6
7
8
// Context代表当前请求上下文
type Context struct {
...

// 当前请求的handler链条
handlers []ControllerHandler
index int // 当前请求调用到调用链的哪个节点
}

第三步,来实现链条调用方式。

为了控制实现链条的逐步调用,我们为Context实现一个Next方法。这个Next方法每调用一次,就将这个控制器链路的调用控制器,往后移动一步。继续在框架文件夹中的context.go文件里写:

1
2
3
4
5
6
7
8
9
10
// 核心函数,调用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;
  • 第二个是在每个中间件的逻辑代码中,用于调用下个中间件。

这里要注意,index下标表示当前调用Next要执行的控制器序列,它的初始值应该为-1,每次调用都会自增1,这样才能保证第一次调用的时候index为0,定位到控制器链条的下标为0的控制器,即第一个控制器。

在框架文件夹context.go的初始化Context函数中,代码如下:

1
2
3
4
5
6
7
// NewContext 初始化一个Context
func NewContext(r *http.Request, w http.ResponseWriter) *Context {
return &Context{
...
index: -1,
}
}

被调用的第一个地方,在入口处调用的代码,写在框架文件夹中的core.go文件中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 所有请求都进入这个函数, 这个函数负责路由分发
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存放我们的测试中间件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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图的左边部分。

很明显,现有的函数没有包含注册中间件逻辑,所以我们需要为Group和Core两个结构增加注册中间件入口,要设计两个地方:

  • Core和Group单独设计一个Use函数,为其数据结构负责的路由批量设置中间件
  • 为Core和Group注册单个路由的Get/Post/Put/Delete函数,设置中间件

先看下批量设置中间件的Use函数,我们在框架文件夹中的core.go修改:

1
2
3
4
// 注册中间件
func (c *Core) Use(middlewares ...ControllerHandler) {
c.middlewares = append(c.middlewares, middlewares...)
}

和框架文件夹中的group.go中修改:
1
2
3
4
// 注册中间件
func (g *Group) Use(middlewares ...ControllerHandler) {
g.middlewares = append(g.middlewares, middlewares...)
}

注意下这里的参数,使用的是Golang的可变参数,这个可变参数代表,我可以传递0~n个ControllerHandler类型的参数,这个设计会增加函数的易用性。它在业务文件夹中使用起来的形式是这样的,在main.go中:
1
2
3
4
5
6
7
8
// core中使用use注册中间件
core.Use(
middleware.Test1(),
middleware.Test2())

// group中使用use注册中间件
subjectApi := core.Group("/subject")
subjectApi.Use(middleware.Test3())

再看单个路由设置中间件的函数,我们也使用可变参数,改造注册路由的函数(Get/Post/Delete/Put),继续在框架文件夹中的core.go里修改:
1
2
3
4
5
6
7
8
9
// 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 改造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,我们注册路由的使用方法就可以变成如下形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 注册路由规则
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一起放在一个数组中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 获取某个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文件夹里写:
1
2
3
4
5
6
7
8
// 匹配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类图如下:

让我们简要回顾下改造过程。

第一步使用控制器链路,我们改造了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的进程模型是单进程、多协程。

在Golang的这种模型中,每个协程是独立且平等的,即使是创建子协程的父协程,在Goroutine中也无法管理子协程。所以,每个协程需要自己保证不会外抛panic,一旦外抛panic了,整个进程就认为出现异常,会终止进程。

这一点搞清楚了,再看Recovery为什么必备就很简单。在net/http处理业务逻辑的协程中,要捕获在自己这个协程中抛出的panic,就必须自己实现Recovery机制。

而Recovery中间件就是用来为每个协程增加Recovery机制的。我们在框架的middleware文件夹中增加recovery.go存放这个中间件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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方法,对所有的路由都设置这个中间件。
1
core.Use(middleware.Recovery())

小结

今天我们最终为自己的框架增加了中间件机制。中间件机制的本质就是装饰器模型,对核心的逻辑函数进行装饰、封装,所以一开始我们就使用函数嵌套的方式实现了中间件机制。

但是实现之后,我们发现函数嵌套的弊端:一是不优雅,二是无法批量设置中间件。所以我们引入了pipeline的思想,将所有中间件做成一个链条,通过这个链条的调用,来实现中间件机制

最后,我们选了最基础的Recovery中间件演示如何具体实现,一方面作为中间件机制的示例,另一方面,也在功能上为我们的框架增强了健壮性。

中间件机制是我们必须要掌握的机制,很多Web框架中都有这个逻辑。在架构层面,中间件机制就相当于,在每个请求的横切面统一注入了一个逻辑。这种统一处理的逻辑是非常有用的,比如统一打印日志、统一打点到统计系统、统一做权限登录验证等。

思考题

现在希望能对每个请求都进行请求时长统计,所以想写一个请求时长统计的中间件,在日志中输出请求URI、请求耗时。不知道你如何实现呢?

0%