Gin Web 框架中 Middleware 的实现原理
7月 2, 2020
Gin 和很多 Web 框架一样实现了 middleware(中间件)的功能,通过 Gin 提供的中间件,我们在业务逻辑处理每个请求之前进行一些通用的逻辑,比如身份校验、数据解密、签名认证、服务限流等功能。
使用方法 #
看起来很简单,只需要 r.Use()
你自定义的 middleware 就可以了。
func main() {
// Creates a router without any middleware by default
r := gin.New()
// Global middleware
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
// By default gin.DefaultWriter = os.Stdout
r.Use(gin.Logger())
// Recovery middleware recovers from any panics and writes a 500 if there was one.
r.Use(gin.Recovery())
// Per route middleware, you can add as many as you desire.
r.GET("/benchmark", MyBenchLogger(), benchEndpoint)
// Authorization group
// authorized := r.Group("/", AuthRequired())
// exactly the same as:
authorized := r.Group("/")
// per group middleware! in this case we use the custom created
// AuthRequired() middleware just in the "authorized" group.
authorized.Use(AuthRequired())
{
authorized.POST("/login", loginEndpoint)
authorized.POST("/submit", submitEndpoint)
authorized.POST("/read", readEndpoint)
// nested group
testing := authorized.Group("testing")
testing.GET("/analytics", analyticsEndpoint)
}
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
自定义的中间件方法定义如下,这里 CheckCost
的功能是记录一个请求的处理时间。
func CheckCost() gin.HandlerFunc {
return func(c *gin.Context) {
//请求前获取当前时间
nowTime := time.Now()
//请求处理
c.Next()
//处理后获取消耗时间
costTime := time.Since(nowTime)
url := c.Request.URL.String()
fmt.Printf("the request URL %s cost %v\n", url, costTime)
}
}
其中 c.Next()
是其他的中间件加上实际的业务逻辑处理内容,至此中间件的使用方法就介绍完了,下面看下内部原理。
内部原理 #
先看 Use 的实现如下,可见 Gin 将中间件的处理流程放在了 group.Handlers
里面统一执行,这样一来按照 slice 的顺序先 Use
的就会先执行。
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
那么这个 handlers 的 Slice 是在哪一步被执行的呢?关于这里的调度我们仅看到了上面提到的 c.Next()
,非常不全面,不利于全局视角。于是我们需要重新看下 Gin 的请求处理流程,一般而言 Gin 的启动方法是 r.Run()
,而 Run
的实现如下:
// gin/gin.go
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
这里的关键就是使用了 Go 标准库里的 http.ListenAndServe(address, engine)
了,也就是说已经进入了标准库处理请求的流程,那么继续点 http.ListenAndServe
进去看:
// http/server.go
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
通过上面这段代码看到其上层 Gin 的 engine
已经转化成了标准库的层面里的 Handler
了。
// http/server.go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Handler
是一个 interface,说明 Gin 的 engine 已经实现了 ServeHTTP
方法,那么我们这就去看 Gin 的 ServeHTTP
是如何实现的吧。
// gin/gin.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)
}
上面这段代码里,Gin 会复用对象池里的 context 对象,这样可以降低垃圾回收带来的性能消耗,除此以外,核心的代码逻辑就是 handleHTTPRequest
了,我们继续进入其代码看。
// gin/gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
// 省略 。。。
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
// 省略 。。。
break
}
// 省略 。。。
c.handlers = engine.allNoRoute
serveError(c, http.StatusNotFound, default404Body)
}
其中 c.Next()
的实现如下:
// gin/context.go
// Next should be used only inside middleware.
// It executes the pending handlers in the chain inside the calling handler.
// See example in GitHub.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
很明显,上面这两段代码已经有我们想要的答案了,最核心的就是 c.handlers[c.index](c)
,它来将 handlers 中的我们之前写好的闭包函数一个一个进行执行。在众多 handlers
中,除了请求处理函数是最后一个,其余都是返回一个闭包的中间件函数。
而之前自定义中间件函数时使用的 c.Next()
也只能在自己编写的中间件内部使用,不能出现在其他地方。 c.index
在一个 context 对象中是一个有状态的变量,所以即使在中间件里进行 c.Next()
也不会重复执行其后面的中间件,可以说是一段设计非常巧妙的代码了。