本文代码地址:https://gitee.com/lymgoforIT/gee-web/tree/master/day5-middleware
本文是 7天用Go从零实现Web框架Gee教程系列
的第五篇。
- 设计并实现
Web
框架的中间件(Middlewares
)机制。 - 实现通用的
Logger
中间件,能够记录请求到响应所花费的时间,代码约50
行
中间件是什么
中间件(middlewares
),简单说,就是非业务的技术类组件。Web
框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。因此,对中间件而言,需要考虑2
个比较关键的点:
- 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在
Handler
中手工调用没有多大的优势了。 - 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。
那对于一个 Web
框架而言,中间件应该设计成什么样呢?接下来的实现,基本参考了 Gin
框架。
中间件设计
Gee
的中间件的定义与路由映射的 Handler
一致,处理的输入是Context
对象。插入点是框架接收到请求初始化Context
对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context
进行二次加工。另外通过调用(*Context).Next()
函数,中间件可等待用户自己定义的 Handler
处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee
的中间件支持用户在请求被处理的前后,做一些额外的操作。举个例子,我们希望最终能够支持如下定义的中间件,c.Next()
表示等待执行的其他中间件或用户的Handler
:
day4-group/gee/logger.go
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
另外,支持设置多个中间件,依次进行调用。
我们上一篇文章分组控制 Group Control
中讲到,中间件是应用在RouterGroup
上的,应用在最顶层的 Group
,相当于作用于全局,所有的请求都会被中间件处理。那为什么不作用在每一条路由规则上呢?作用在某条路由规则,那还不如用户直接在 Handler
中调用直观。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。
我们之前的框架设计是这样的,当接收到请求后,匹配路由,该请求的所有信息都保存在Context
中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在Context
中,依次进行调用。为什么依次调用后,还需要在Context
中保存呢?因为在设计中,中间件不仅作用在处理流程前,也可以作用在处理流程后,即在用户定义的 Handler
处理完毕后,还可以执行剩下的操作。
为此,我们给Context
添加了2
个参数(handlers
和index
),定义了Next
方法:
day4-group/gee/context.go
type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
Params map[string]string
// response info
StatusCode int
// middleware
handlers []HandlerFunc
index int
}
func newContext(w http.ResponseWriter, req *http.Request) *Context {
return &Context{
Path: req.URL.Path,
Method: req.Method,
Req: req,
Writer: w,
index: -1,
}
}
func (c *Context) Next() {
// 注意这里是本次请求上下文中的c的index++,而不是一个局部变量index++
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
// 可以主动调用Fail,从而终止后续当前链上后续handlers的调用了,类似Gin框架的Abort
func (c *Context) Fail(code int, err string) {
c.index = len(c.handlers)
c.JSON(code, H{"message": err})
}
注意这里是本次请求上下文c中的c.index++,而不是一个局部变量index++,记住这个点会更好的理解后面的内容以及文末的第一个问题
index
是记录当前执行到第几个中间件,当在中间件中调用Next
方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next
方法之后定义的部分。如果我们将用户在映射路由时定义的Handler
添加到c.handlers
列表中,结果会怎么样呢?想必你已经猜到了。
func A(c *Context) {
part1
c.Next()
part2
}
func B(c *Context) {
part3
c.Next()
part4
}
假设我们应用了中间件A
和 B
,和路由映射的 Handler
。c.handlers
是这样的[A, B, Handler]
,c.index
初始化为-1
。调用c.Next()
,接下来的流程是这样的:
c.index++
,c.index
变为0
0 < 3
,调用c.handlers[0]
,即A
- 执行
part1
,调用c.Next()
c.index++
,c.index
变为1
1 < 3
,调用c.handlers[1]
,即B
- 执行
part3
,调用c.Next()
c.index++
,c.index
变为2
2 < 3
,调用c.handlers[2]
,即Handler
Handler
调用完毕,返回到B
中的part4
,执行part4
part4
执行完毕,返回到A
中的part2
,执行part2
part2
执行完毕,结束。
一句话说清楚重点,最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2
。恰恰满足了我们对中间件的要求,接下来看调用部分的代码,就能全部串起来了。
代码实现
定义Use
函数,将中间件应用到某个 Group
。
day4-group/gee/gee.go
// Use is defined to add middleware to the group
func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
group.middlewares = append(group.middlewares, middlewares...)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
var middlewares []HandlerFunc
for _, group := range engine.groups {
if strings.HasPrefix(req.URL.Path, group.prefix) {
middlewares = append(middlewares, group.middlewares...)
}
}
c := newContext(w, req)
c.handlers = middlewares
engine.router.handle(c)
}
ServeHTTP
函数也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件,在这里我们简单通过URL
的前缀来判断。得到中间件列表后,赋值给 c.handlers
。
handle
函数中,将从路由匹配得到的 Handler
添加到 c.handlers
列表中,执行c.Next()
。
day4-group/gee/router.go
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
key := c.Method + "-" + n.pattern
c.Params = params
c.handlers = append(c.handlers, r.handlers[key])
} else {
c.handlers = append(c.handlers, func(c *Context) {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
})
}
c.Next()
}
使用 Demo
func onlyForV2() gee.HandlerFunc {
return func(c *gee.Context) {
// Start timer
t := time.Now()
// if a server error occurred
c.Fail(500, "Internal Server Error")
// Calculate resolution time
log.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
func main() {
r := gee.New()
r.Use(gee.Logger()) // global midlleware
r.GET("/", func(c *gee.Context) {
c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
})
v2 := r.Group("/v2")
v2.Use(onlyForV2()) // v2 group middleware
{
v2.GET("/hello/:name", func(c *gee.Context) {
// expect /hello/geektutu
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})
}
r.Run(":9999")
}
gee.Logger()
即我们一开始就介绍的中间件,我们将这个中间件和框架代码放在了一起,作为框架默认提供的中间件。在这个例子中,我们将gee.Logger()
应用在了全局,所有的路由都会应用该中间件。onlyForV2()
是用来测试功能的,仅在v2
对应的 Group
中应用了。
接下来使用 curl
测试,可以看到,v2 Group 2
个中间件都生效了。
$ curl http://localhost:9999/
>>> log
2019/08/17 01:37:38 [200] / in 3.14µs
(2) global + group middleware
$ curl http://localhost:9999/v2/hello/geektutu
>>> log
2019/08/17 01:38:48 [200] /v2/hello/geektutu in 61.467µs for group v2
2019/08/17 01:38:48 [200] /v2/hello/geektutu in 281µs
QA
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
fmt.Printf("index:%d\n", c.index)
c.handlers[c.index](c)
}
}
您好,想请教下为什么这个Next函数需要遍历c.handlers?因为我看hadlers函数会带c.Next(),如下Logger:
func Logger() HandlerFunc {
return func(c *Context) {
// Start timer
t := time.Now()
// Process request
c.Next()
// Calculate resolution time
log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
}
}
所以,为什么不直接这样写?
func (c *Context) Next() {
c.index++
c.handlers[c.index](c)
}
答:不是所有的中间件都会调用 Next()。手工调用 Next(),一般用于在请求前后各实现一些行为。如果中间件只作用于请求前,可以省略调用Next(),算是一种兼容性比较好的写法吧。
func (c *Context) Next() {
c.index++
s := len(c.handlers)
for ; c.index < s; c.index++ {
fmt.Printf("index:%d\n", c.index)
c.handlers[c.index](c)
}
}
A:c.index 为啥要初始化为-1,我初始化为0 去掉c.index++ 一直死循环
B: Next() 函数一开始调用了 c.index++,下标恰好从 0 开始。
A: 我的意思是 index 直接初始化为0, Next()就不用c.index++了,我自己尝试初始化为0 ,去调c.index++ 一直死循环
B : 你把Next() 第一行的 c.index++ 去掉,就会一直卡在第一个中间件上了,必然死循环。每调用一次 Next(),c.index 得 +1,不然 c.index 就会一直是 0。死循环之后,是不会走到 for 语句后面的 c.index++ 的。
原文地址:https://geektutu.com/post/gee-day5.html