go原理系列| 理解gin中间件的运行

7,391 阅读4分钟

中间件原理

中间件(middleware),其原理就是对一个方法进行包裹装饰,然后返回同类型的方法,在Python中又名装饰器,甚至成为了Python的语法糖。
应用场景大多是需要对某一类函数进行通用的前置或者后置处理。
最常见的就是在web开发中,执行相应请求的handler函数前,需要对token合法性进行校验,在handler执行完后需要对处理结果产生的err进行记录和上报,这些都是通用的逻辑,并不需要在每个handler中都编写一遍。
实现一个最简单也最经典的中间件例子,计算函数的执行时间、记录日志。

func Hi(writer http.ResponseWriter, request *http.Request){
	time.Sleep(time.Second*1)
	rsp:=map[string]interface{}{"errcode":0,"msg":"hi"}
	rspByte,_:=json.Marshal(rsp)
	writer.Write(rspByte)
}
//中间件
func TimeUse(next http.HandlerFunc)http.HandlerFunc{
	return func(writer http.ResponseWriter, request *http.Request) {
		start:=time.Now()
		next(writer,request)
		fmt.Printf("cost %f second",time.Since(start).Seconds())
	}
}
func Log(next http.HandlerFunc)http.HandlerFunc{
	return func(writer http.ResponseWriter, request *http.Request) {
		next(writer,request)
		log.Println("run success")
	}
}

func main() {
	mx:=http.NewServeMux()
    mx.HandleFunc("/hi", Log(TimeUse(Hi)))
    http.ListenAndServe(":8080",mx)
}

我们在需要使用中间件的地方嵌套包裹,就实现了中间件的效果。但是会发现,每当新增一个中间件都需要这样包裹,最后造成的代码是这样的。
   image.png
当中间件数量多起来之后,这样的写法着实会令人头皮发麻,当然要优化一下。
初始化需要执行的中间件数组,使用for循环进行嵌套包裹,这样的好处就是需要改动一组使用相同中间件的函数时,只需要更改中间链数组就可以了,这也是比较通用的写法。

type middle func (next http.HandlerFunc)http.HandlerFunc

func addMiddle(h http.HandlerFunc,m ...middle)http.HandlerFunc{

	for i:=len(m)-1;i>=0;i--{
		h=m[i](h)
	}
	return h
}


func main() {
	mx:=http.NewServeMux()
	m1:=[]middle{TimeUse,Log}
    mx.HandleFunc("/hi", addMiddle(Hi,m1...))
    mx.HandleFunc("/hi2", addMiddle(Hi1,m1...))
    mx.HandleFunc("/hi3", addMiddle(Hi2,m1...))
    mx.HandleFunc("/hi4", addMiddle(Hi3,m1...))
    http.ListenAndServe(":8080",mx)
}

采用倒序包裹是为了对应添加中间件顺序,越往后添加的中间件越贴近需要执行的逻辑。
这个过程可以想象成以下的模型,其实就是典型的递归调用。

   image.png

gin的中间件


gin初始化一个handler,并添加中间件。

func test03() {
	r := gin.Default()
	r.Handle("GET", "/hello", func(context *gin.Context) {
		fmt.Println("one")
		context.Next()
		fmt.Println("one-back")
	}, func(context *gin.Context) {
		fmt.Println("myHandler")
	})
	r.Run(":8080")
}

func main() {
    test03()
}

查看handle源码,添加的中间件最终是与处理逻辑handler一起组装成了HandlersChain,毕竟他们本质上一样的,都是HandlerFunc,只不过业务逻辑是最后一个执行的handlerFunc。

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

// HandlersChain defines a HandlerFunc array.
type HandlersChain []HandlerFunc

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 计算绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
    // 将handler与原有的合并,组成新的HandlersChain
	handlers = group.combineHandlers(handlers)
    // 注册到全局路由树中
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

//合并处理链
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
}

在gin处理http请求的SeverHttp()中,我们可以看到,根据本次请求的路径从路由树中获得本次对应的HandlersChain,然后赋值给ctx.Handlers,接着执行ctx.Next()。

// Find route in tree
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
            //赋值给context
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
            //开始执行context的handlerChain
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}

为了方便分析ctx的执行原理,这里模仿gin的context结构模仿定义了一个简化的GinContext,重点关注next()函数,index需要初始化为-1

type Handler func(gc *GContext)
type HanclerChain []Handler

type GContext struct {
	Handlers HanclerChain
	index    int
}

func (g *GContext) Next() {
	g.index++
	for g.index < len(g.Handlers) {
		g.Handlers[g.index](g)
		g.index++
	}
}

func (g *GContext) Handle(hs ...Handler) {
	g.Handlers = append(g.Handlers, hs...)
}
func (g *GContext) Start() {
	g.Next()
}

func main() {
	gc := &GContext{
		index:    -1,
		Handlers: HanclerChain{},
	}
	gc.Handle(func(gc *GContext) {
		fmt.Println("one")
		gc.Next()
		fmt.Println("one-back")
	}, func(gc *GContext) {
		fmt.Println("two")
		gc.Next()
		fmt.Println("two-back")
	}, func(gc *GContext) {
		fmt.Println("three")
		gc.Next()
		fmt.Println("three-back")
	}, func(gc *GContext) {
		fmt.Println("four")
		//gc.Next()
		fmt.Println("four-back")
	})
	gc.Start()
}

运行结果是
   image.png
我们在编写gin的中间件时,如果需要后置处理,是需要执行context.Next()的,很显然,这是一个递归调用,只是通过串联context,使中间件可以主动把握递归调用下一层的时机,甚至中止处理链的继续执行,如果没有调用next(),则在本次handler执行结束后直接执行下一个。中间件的一般流程是,先调用前置动作,使函数往下执行,往上返回,然后执行后置动作。

下面是GinContext的两个执行模型。分别对应中间件中使用Next()和不使用Next()。

   image.png

通过对gin源码的分析,可以看出gin.Context的设计还是比较巧妙的,context在递归调用过程中很好的发挥了上下文的作用,穿针引线,携带着全局的请求信息,暴露向下层调用的api。