Golang Web入门(3):如何优雅的设计中间件

摘要

上一篇文章中,咱们已经能够实现一个性能较高,且支持RESTful风格的路由了。可是,在Web应用的开发中,咱们还须要一些能够被扩展的功能。git

所以,在设计框架的过程当中,应该留出能够扩展的空间,好比:日志记录、故障恢复等功能,若是咱们把这些业务逻辑全都塞进Controller/Handler中,会显得代码特别的冗余,杂乱。github

因此在这篇文章中,咱们来探究如何更优雅的设计这些中间件。数组

1 耦合的实现方式

好比咱们要实现一个日志记录的功能,咱们能够用这种简单粗暴的方式:bash

package main

import (
	"fmt"
	"net/http"
	"time"
)

func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
	record(r.URL.Path)
	fmt.Fprintf(w, "Hello World !")
}

func main() {
	http.HandleFunc("/hello", helloWorldHandler)
	http.ListenAndServe(":8000", nil)
}

func record(path string)  {
	fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + path)
}
复制代码

若是这样作的话,确实是实现了咱们的目标,记录了访问的日志。app

可是,这样一点都不优雅。框架

每个Handler内部都须要调用record函数,而后再把须要记录的path做为参数传进record函数中。函数

若是这样作,无论咱们须要添加什么样的额外功能,都必须得把这个额外的功能和咱们的业务逻辑紧紧地绑定到一块儿,不能实现扩展功能与业务逻辑间的解耦。post

2 将记录与实现解耦

既然在上面的实现中,记录日志和业务实现彻底的耦合在了一块儿,那么咱们能不能把他们的业务实现解耦开来呢?性能

来看这段代码:ui

func record(w http.ResponseWriter, r *http.Request)  {
	path := r.URL.Path
	method := r.Method
	fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}

func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
	record(w ,r)
	fmt.Fprintf(w, "Hello World !")
}
复制代码

在这里,咱们已经把业务实现和日志记录的耦合给解开了一部分。

咱们只须要在业务代码中,调用record(w,r)函数,把请求的内容做为参数传进record函数中,而后在record这个方法内记录日志。这个时候,咱们能够在方法内部任意的处理请求,保存如请求路径、请求方法等数据。而这个过程,对业务实现是透明的

这样作的话,咱们只须要在处理业务逻辑的Handler中调用函数,而后把参数传进去。而这个函数的具体实现,则是与业务逻辑无关的。

那么,有没有办法能够把业务逻辑和扩展功能彻底分开,让业务代码里只有业务代码,使代码变得更加整洁呢?咱们接着往下看。

3 设计中间件

咱们在上一篇文章里面,分析了httprouter这个包的实现。因此咱们直接对他动手,修改他的代码,增长一个AddBeforeHandle方法,使得这个路由具备扩展性。

注意,这里的AddBeforeHandle方法是做者本身编写的,具体的实现过程能够看后文。

3.1 效果

在此以前,咱们来看看效果:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/julienschmidt/httprouter"
)

func Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Hello World!\n")
}

func record(w http.ResponseWriter, r *http.Request){
	path := r.URL.Path
	method := r.Method
	fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}


func main() {
	router := httprouter.New()
	router.AddBeforeHandle(record)
	router.GET("/hello", Hello)
	log.Fatal(http.ListenAndServe(":8080", router))
}
复制代码

这部分的代码和上一篇的几乎彻底同样。也是建立一个路由,将/hello这个路径和Hello这个处理器绑定在GET的这颗前缀树中,而后开始监听8080端口。

这里比较重要的是main方法里面的第二行:

router.AddBeforeHandle(record)
复制代码

从方法名能够看出,这个方法是在Handle以前增长了一个处理过程。

再看看参数,就是咱们上面提到的记录访问日志的方法,这个方法记录了请求的URL,请求的方法,以及时间。

而在咱们的Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params)函数中,已经不包含任何其余的业务逻辑了。

此时,这个Handler专一于处理业务逻辑,至于别的,交给别的函数去实现。这样,就实现了彻底的解耦

下面咱们来看看具体的实现过程:

3.2 具体实现

先来看看AddBeforeHandle这个方法:

func (r *Router) AddBeforeHandle(fn func(w http.ResponseWriter, req *http.Request))  {
	r.beforeHandler = fn
}
复制代码

这个方法很简单,也就是接收一个处理器类型的参数,而后赋值给Router中的字段beforeHandler

这个名为beforeHandler字段也是咱们新增在Router中的,相信你也能看得出来了,所谓的AddBeforeHandle方法,就是把咱们传进去的处理函数,保存在Router中,在须要的时候调用他。

那么咱们来看看,何时会调用这个方法。下面列出的这个方法,在上一篇文章有提到,是关于httprouter是如何处理路由的:

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
	if root := r.trees[req.Method]; root != nil {
		if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
		    if r.beforeHandler != nil{
				r.beforeHandler(w, req)
			}
			if ps != nil {
				handle(w, req, *ps)
				r.putParams(ps)
			} else {
				handle(w, req, nil)
			}
			return
		} 
	}
    ...
}
复制代码

注意看,router在找到了Handler,准备执行以前,咱们添加了这么几行:

if r.beforeHandler != nil{
	r.beforeHandler(w, req)
}
复制代码

也就是说,若是咱们以前调用了AddBeforeHandle方法,给beforeHandler这个字段赋了值,那么他就不会为nil,而后调用这个函数。这也就实现了咱们的目的,在处理请求以前,先执行咱们设置的函数。

3.3 思考

如今咱们已经实现了一个彻底解耦的中间件。而且,这个中间件是能够任意配置的。你能够拿来作日志记录,也能够作权限校验等等,并且这些功能还不会对Handler中的业务逻辑形成影响。

若是你是个Java开发者,你可能会以为这个很像Filter,或者是AOP

可是,和过滤器不一样的是,咱们不只能够在请求到来以前处理,也能够在请求完成以后处理。好比这个请求发生了一些panic,你能够在最后处理它,或者你能够记录这个请求的时间等等,你要作的,只是在Handle方法以后,调用你所注册的方法。

好比:

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
	if root := r.trees[req.Method]; root != nil {
		if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
		    if r.beforeHandler != nil{
				r.beforeHandler(w, req)
			}
			if ps != nil {
				handle(w, req, *ps)
				r.putParams(ps)
			} else {
				handle(w, req, nil)
			}
			if r.afterHandler != nil {
				r.afterHandler(w, req)
			}
			return
		} 
	}
    ...
}
复制代码

咱们只是添加了一个afterHandler方法,就是这么的简单。

那么问题来了:如今这样的处理操做,咱们仅仅只能在请求前和请求后各自添加一个中间件。若是咱们想要添加任意多个中间件,该怎么作呢?

能够先本身思考一下,而后咱们来看看在gin中,是怎么实现的。

4 Gin的中间件

4.1 使用

众所周知,在阅读源码以前,必定要先看看他是怎么用的:

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
)

func Hello(ctx *gin.Context) {
	fmt.Fprint(ctx.Writer, "Hello World!\n")
}

func main() {
	router := gin.New()
	router.Use(gin.Logger(), gin.Recovery())
	router.GET("/hello", Hello)
	router.Run(":8080")
}
复制代码

能够看到,在gin中,使用中间件的方法和上文中咱们所设计的是差很少的。都是业务和中间件彻底解耦,而且在注册路由的时候,添加进去。

可是咱们注意到,在gin中是不分Handle以前仍是Handle以后的。那么他是如何作到的呢,咱们来看看源码。

4.2 源码解释

先从Use方法看起:

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}
复制代码

在这里咱们先无论group这个东西,他是路由分组,和咱们这篇文章没有关系,咱们先无论他。咱们只须要看到append方法。Use方法就是把参数里面的函数,所有增长到group.Handlers中。这里的group.Handlers,是一个Handler类型的数组。

因此,在gin中,每个中间件,也是Handler类型的。

在上一节咱们留了一个问题,要怎么实现多个中间件。答案就在这里了,用数组保存。

那么问题又来了:怎么保证调用的顺序呢?

咱们继续往下看看路由的注册:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}
复制代码

这里是否是也有点熟悉呢?和上一篇文章提到的httprouter很类似,咱们直接看group.handle

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	absolutePath := group.calculateAbsolutePath(relativePath)
	handlers = group.combineHandlers(handlers)
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}
复制代码

在这段代码中,第一行关于path的咱们先无论,这个也是和路由分组有关的,简单来讲就是拼接出完整的请求path

先看看第二行,方法名是combineHandlers,咱们能够猜想一下这个方法的做用,把各个Handler结合起来。看看详细的代码:

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
}
复制代码

先解释一下,这里返回的HandlersChain类型,是Handler的数组。

也就是说,在这个方法里面,把以前放入group中的中间件,和当前路由的Handler,组合成一个新的数组。

而且,中间件在前面,路由Handler在后面。注意,这个顺序很重要

而后咱们继续往下,执行完这个方法以后执行的就是addRoute方法了。在这里不展开讲。因此最重要的是,这里把中间件和Handler全都组合在了一块儿,绑定到了这个前缀树上。

到了这里注册方面的内容已经结束了,咱们来看看他是怎么处理各个中间件的调用顺序

由于咱们的目的是看路由是怎么处理请求的,因此咱们直接看ginServeHTTP方法:

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)
}
复制代码

这里要注意的是*Context,他是对请求的封装,包含了有responseWriter*http.Request等。

咱们继续往下看看handleHTTPRequest(c)这个方法:

func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
	...
    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
		}
		...
	}
	...
}
复制代码

在这个方法中,其实和以前咱们研究的httprouter是很类似的。也是先根据请求方法找到相对应的前缀树,而后获取相对应的Handler,并把获取到的handler数组保存在Context中。

这里咱们注意看c.Next()方法,他是gin中关于中间件的调用最精妙的部分。咱们来看看:

func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}
复制代码

咱们能够看到,当调用这个Next()方法的时候,会增长保存在Context中的下标,而后根据这个下标的顺序执行handler

而在前面咱们有提到,咱们把中间件排在了这个handler数组的前面,先执行中间件,而后最后才是执行用户自定义的handler

咱们再来看看日志记录这个中间件:

func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
	...
	return func(c *Context) {
		//开始计时
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		c.Next()
		...
		// Stop timer
		param.TimeStamp = time.Now()
		param.Latency = param.TimeStamp.Sub(start)
		...
	}
}
复制代码

能够看到,先开始计时,而后调用了c.Next()这个方法,而后才结束计时。

那么咱们能够由此推断,c.Next()后面的代码,是执行完用户自定义的Handler才执行的。

也就是说,其实中间件的业务逻辑是这样的:

func Middleware(c *gin.Context){
    //请求前执行
    c.Next()
    //请求后执行
}
复制代码

5 写在最后

首先,谢谢你能看到这里。

简单的来说,咱们应该考虑解耦合,使得业务代码能够专一于业务,中间件专一于实现功能。为了实现这点,咱们能够修改路由的实现逻辑,在执行Handler的先后加入中间件的调用。

在本文中,可能会有不少的疏漏。若是在阅读的过程当中,有哪些解释不到位,或者做者的理解出现了一些差错,也请你留言指正。

再次感谢~

PS:若是有其余的问题,也能够在公众号找到做者。而且,全部文章第一时间会在公众号更新,欢迎来找做者玩~

相关文章
相关标签/搜索