Gin Web 框架中 Middleware 的实现原理

Gin Web 框架中 Middleware 的实现原理

7月 2, 2020
源码分析, Golang

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() 也不会重复执行其后面的中间件,可以说是一段设计非常巧妙的代码了。

参考资料 #

Gin 源码学习(四)丨Gin 对请求的处理流程

本文共 1255 字,上次修改于 Dec 6, 2024,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» Go 语言中 Goroutine 的并发数量控制

» Gin Web 框架中 Validate 使用总结

» Go 程序取消子 Goroutine 的几种方式

» Go 标准库中涉及 I/O 操作的几个包的区别

» 浅谈 Django-REST-Framework 的设计与源码