这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
引子
首先,让我们抛开Gin
的实现细节看一下我们都用中间件来做些什么事情:
- 权限校验
- 日志记录
- 接口出入惨校验
....
相信学过Java的同学初次接触Gin
的中间件的时候都会有这样的感觉:
"这不就是过滤器/拦截器吗"
没错,从实用主义的角度出发来看Gin
的所谓的中间件本质上是向开发人员提供的一个通用的流程控制。 我们可以将一些通用的流程控制代码集中到中间件中, 比如权限校验, 日志记录等待。
假如要让我们自己实现一个类似Gin的中间机制,你会怎么做呢?
首先让我们来思考一个中间件系统都应该有什么功能:
- 支持自由组合中间件函数
- 该系统会按照我们组合中间件的顺序进行调用中间件函数
- 中间的函数可以拦截请求, 即请求不被业务函数或者下一个中间执行(此场景通常用于权限校验机制的实现)
综上可以得知我们的中间系统主要目的管理就是管理中间件函数的调用关系。你可以通过数组或者链表来管理中间函数,但是函数作为Golang的一等公民,我们自然是要使用闭包机制来实现一个简单的中间件系统。
既然要使用闭包机制来实现中间件的功能,实际上就是我们的中间函数要做的就是对业务函数的包装并返回一个包装后的业务函数,如此我们便可以实现自由组合中间函数。
func Middleware(handler func()) func() {
return func() {
// do something before handle
handler()
// do something after handle
}
}
我们将业务函数定义为一个无入参出参的函数, 为了测试我们实现一个会随机休眠0至2秒的函数,用于模拟业务处理。
func FakeBusiness() {
n := rand.Intn(3)
time.Sleep( time.Duration(n) * time.Second )
fmt.Printf("Job done time cost %ds\n", n)
}
接下来我们定义三个中间函数,并将他们和业务函数组装起来
//Log 记录日志
func Log(next func()) func() {
return func() {
fmt.Printf("[Log] Before Log \n")
next()
fmt.Printf("[Log] After Log \n")
}
}
//RandomAbort 随机拒绝请求, 只要不执行next函数后续的中间函数以及业务函数就不会被执行到
func RandomAbort(next func()) func() {
return func() {
n := rand.Intn(10)
if n % 2 == 0 {
fmt.Printf("[RandomAbort] abort %d ! \n", n)
return
}
// 奇数才调用
fmt.Printf("[RandomAbort] n=%d Not abort! execute next\n", n)
next()
fmt.Printf("[RandomAbort] execute next finished \n")
}
}
//Timecost 记录业务函数耗时
func Timecost(next func()) func() {
return func() {
start := time.Now()
fmt.Printf("[Timecost] Before handle %d\n", start.Unix())
next()
fmt.Printf("[Timecost] After handle %d timecost %dms\n",
time.Now().Unix(), time.Since(start).Milliseconds())
}
}
组合起来(由于中间函数返回的是包装后的业务函数,因此我们可以自由组合这些中间件。)
func main() {
rand.Seed(time.Now().Unix())
handlerChain := Timecost(Log(RandomAbort(FakeBusiness)))
handlerChain()
}
组合起来后的中间件以及业务函数看起来是这样的:
可以看出组合起来的中间件以及业务函数本质上就是一个调用栈(callstack
)
Gin中间件的实现
相较于闭包而言, Gin
选择了一种认知负担较小的实现方式, 其底层使用了一个函数数组来保存这些中间件,定义如下所示:
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)
// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc
每当有一个请求到达服务端时, Gin
就会为这个请求分配一个Context
, 该上下文中保存了这个请求对应的处理器链, 以及一个索引idnex
用于记录当前处理到了哪个HandlerFunc
, index
的初始值为-1
type Context struct {
....
handlers HandlersChain
index int8
....
当上下文构造出来后, Gin
就会调用Context
的Next
方法开始处理请求.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
如果要实现上文中的嵌套调用效果,我们只需要在HandlerFunc
中调用Conext
的Next()
方法即可, 此时上下文会在索引上加1, 以执行下一个HandlerFunc
, 当函数调用栈回到当前方法时由于索引已大于HandlerChain
的大小,也就不会出现重复执行的问题。
如果你注意到上文将HandlerChain
得大小强制转换为int8
类型可知Gin
对HandlerFunc
的数量做了限制, 理论上Gin
中仅能支持127个HandlerFunc
, 但是实际上在调用Use
方法添加中件时Gin
会将HandlerChain
得大小和abortIndex
进行比较, 如果大于这个值则直接报错.
// abortIndex represents a typical value used in abort functions.
const abortIndex int8 = math.MaxInt8 >> 1 // 63
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
因此如果要提前结束请求我们直接index
设置成abortIndex
得值即可.
func (c *Context) Abort() {
c.index = abortIndex
}
对上文得例子进行改造后,我们就得到了以下代码:
r := gin.Default()
// time cost
r.Use(func(context *gin.Context) {
start := time.Now()
context.Next()
fmt.Printf("time cost %d ms\n", time.Since(start).Milliseconds())
})
// random abort
r.Use(func(context *gin.Context) {
if rand.Intn(10) % 2 == 0 {
context.JSON(200, map[string]interface{}{
"Message": fmt.Sprintf("Request abort"),
"Code": -1,
})
context.Abort()
}
})
// log
r.Use(func(context *gin.Context) {
fmt.Printf("before handle\n")
context.Next()
fmt.Printf("after handle\n")
})
// business
r.GET("/test", func(context *gin.Context) {
context.JSON(200, map[string]interface{}{
"Message": "HelloWorld",
"Code": 0,
})
})
r.Run(":8080")
总结
Gin的中间件系统底层维护了一个函数数组HandlerChain
以及一个索引index
, 用来存储中间件函数以及接口处理函数。并且:
- 中间件函数会按照添加顺序运行
Abort()
的原理是将索引index
的值设置为abortIndex
,此值比Gin所能支持的HandlerFunc
数量大- 如果要对一个请求进行前置处理和后置处理,调用
Next()
方法即可,调用此方法之前的代码即为前置处理, 之后即为后置处理。