乐趣区

关于go:Go框架深入理解web框架的中间件运行机制

大家好,我是渔夫子。本号新推出「Go 工具箱」系列,意在给大家分享应用 go 语言编写的、实用的、好玩的工具。同时理解其底层的实现原理,以便更深刻地理解 Go 语言。

大家在应用 iris 框架搭建 web 零碎时,肯定会用到中间件。那么你理解中间件的运行机制吗?你晓得为什么在 iris 和 gin 框架的申请处理函数中要加 c.Next()函数吗?本文就和大家一起探索该问题的答案。

一、中间件的根本应用

在 web 开发中,中间件起着很重要的作用。比方,身份验证、权限认证、日志记录等。以下就是各框架对中间件的根本应用。

1.1 iris 框架中间件的应用

package main

import (
    "github.com/kataras/iris/v12"
    "github.com/kataras/iris/v12/context"

    "github.com/kataras/iris/v12/middleware/recover"
)

func main() {app := iris.New()

    // 通过 use 函数应用中间件 recover
    app.Use(recover.New())

    app.Get("/home",func(ctx *context.Context) {ctx.Write([]byte("Hello Wolrd"))
    })

    app.Listen(":8080")
}

1.2 gin 框架中应用中间件

package main

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

func main() {g := gin.New()
    // 通过 Use 函数应用中间件
    g.Use(gin.Recovery())
    
    g.GET("/", func(ctx *gin.Context){ctx.Writer.Write([]byte("Hello World"))
    })

    g.Run(":8000")
}

1.3 echo 框架中应用中间件示例

package main

import (
    v4echo "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {e := v4echo.New()
    // 通过 use 函数应用中间件 Recover
    e.Use(middleware.Recover())
    e.GET("/home", func(c v4echo.Context) error {c.Response().Write([]byte("Hello World"))
        return nil
    })

    e.Start(":8080")
}

首先咱们看下三个框架中应用中间件的共同点:

  • 都是应用 Use 函数来应用中间件
  • 都内置了 Recover 中间件
  • 都是先执行中间件 Recover 的逻辑,而后再输入Hello World

接下来咱们持续剖析中间件的具体实现。

二、中间件的实现

2.1 iris 中间件实现

2.1.1 iris 框架中间件类型

首先,咱们看下 Use 函数的签名,如下:

func (api *APIBuilder) Use(handlers ...context.Handler) {api.middleware = append(api.middleware, handlers...)
}

在该函数中,handlers 是一个不定长参数,阐明是一个数组。参数类型是 context.Handler,咱们再来看 context.Handler 的定义如下:

type Handler func(*Context)

这个类型是不是似曾相识。是的,在注册路由时定义的申请处理器也是该类型。如下:

func (api *APIBuilder) Get(relativePath string, handlers ...context.Handler) *Route {return api.Handle(http.MethodGet, relativePath, handlers...)
}

总结:在 iris 框架上中间件也是一个申请处理器。通过 Use 函数应用中间件,实际上是将该中间件对立退出到了 api.middleware 切片中。该切片咱们在前面再深入研究

2.1.2 iris 中自定义中间件

理解了中间件的类型,咱们就能够依据其规定来定义本人的中间件了。如下:

import "github.com/kataras/iris/v12/context"

func CustomMiddleware(ctx *context.Context) {fmt.Println("this is the custom middleware")
    // 具体的解决逻辑
    
    ctx.Next()}

当然,为了代码格调对立,也能够相似 Recover 中间件那样定义个包,而后定义个 New 函数,New 函数返回的是一个中间件函数,如下:

package CustomMiddleware 

func New() context.Handler {return func(ctx *context.Context) {fmt.Println("this is the custom middleware")
        // 具体的解决逻辑

        ctx.Next()}
}

到此为止,你有没有发现,无论是自定义的中间件,还是 iris 框架中已存在的中间件,在最初都有一行 ctx.Next()代码。那么,该为什么要有这行代码呢?通过函数名能够看到执行下一个申请处理器。再联合咱们在应用 Use 函数应用中间件的时候,是把该中间件处理器退出到了一个切片中。所以,Next 和申请处理器切片是有关系的。这个咱们在下文的运行机制局部具体解释。

2.2 gin 中间件的实现

2.2.1 gin 框架中间件类型

同样先查看 gin 的 Use 函数的签名和实现,如下:

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

在 gin 框架的 Use 函数中,middleware也是一个不定长的参数,其参数类型是 HandlerFunc。而HandlerFunc 的定义如下:

type HandlerFunc func(*Context)

同样,在 gin 框架中注册路由时指定的申请处理器的类型也是 HandlerFunc,即 func(*Context)。咱们再看 Use 中的第 2 行代码 engine.RouterGroup.Use(middleware…)的实现:

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

同样,也是将中间件退出到了路由的 Handlers 切片中。

总结:在 gin 框架中,中间件也是一个申请处理函数。通过 Use 函数应用中间件,实际上也是将该中间件对立退出到了 group.Handlers 切片中。

2.2.2 gin 中自定义中间件

理解了 gin 的中间件类型,咱们就能够依据其规定来定义本人的中间件了。如下:

import "github.com/gin-gonic/gin"

func CustomMiddleware(ctx *gin.Context) {fmt.Println("this is gin custom middleware")
    //    解决逻辑
    ctx.Next()}

当然,为了代码格调对立,也能够相似 Recover 中间件那样返回一个,而后定义个 New 函数,New 函数返回的是一个中间件函数,如下:

func CustomMiddleware() gin.HandlerFunc {return func(ctx *gin.Context) {fmt.Println("this is gin custom middleware")
        //    解决逻辑
        ctx.Next()}
}

同样,在 gin 的中间件中,代码的最初一行也是 ctx.Next() 函数。如果不要这行代码行不行呢?和 iris 的情理是一样的,咱们也在下文的运行机制中解说。

2.3 echo 框架中间件的实现

2.3.1 echo 框架中间件类型

func (e *Echo) Use(middleware ...MiddlewareFunc) {e.middleware = append(e.middleware, middleware...)
}

在 echo 框架中,Use 函数中的 middleware 参数也是一个不定长参数,阐明能够增加多个中间件。其类型是 MiddlewareFunc。如下是 MiddewareFunc 类型的定义:

type MiddlewareFunc func(next HandlerFunc) HandlerFunc

这个中间件的函数类型跟 iris 和 gin 的不一样。该函数类型接管一个 HandlerFunc,并返回一个 HanderFunc。而 HanderFunc 的定义如下:

HandlerFunc func(c Context) error

HanderFunc 类型才是指定路由时的申请处理器类型。咱们再看下 echo 框架中 Use 的实现,也是将 middleware 退出到了一个全局的切片中。

总结:在 echo 框架中,中间件是一个输出申请处理器,并返回一个新申请处理器的函数类型。这是和 iris 和 gin 框架不一样的中央。通过 Use 函数应用中间件,也是将该中间件对立退出到全局的中间件切片中。

2.3.2 echo 中自定义中间件

理解了 echo 的中间件类型,咱们就能够依据其规定来定义本人的中间件了。如下:

import (v4echo "github.com/labstack/echo/v4")

func CustomMiddleware(next v4echo.HandlerFunc) v4echo.HandlerFunc {return func(c v4echo.Context) error {fmt.Println("this is echo custom middleware")
        // 中间件解决逻辑
        return next(c)
    }
}

这里中间件的实现看起来比较复杂,做下简略的解释。依据下面可知,echo 的中间件类型是输出一个申请处理器,而后返回一个新的申请处理器。在该函数中,从第 6 行到第 10 行该函数其实是中间件的执行逻辑。第 9 行的 next(c)实际上是要执行下一个申请处理器的逻辑,相似于 iris 和 gin 中的 ctx.Next()函数。 实质上是用一个新的申请处理器(返回的申请处理器)包装了一下旧的申请处理器(输出的 next 申请处理器)

中间件的定义和应用都介绍了。那么,中间件和具体路由中的申请处理器是如何协同工作的呢?上面咱们介绍中间件的运行机制。

三、中间件的运行机制

3.1 iris 中间件的运行机制

依据上文介绍,咱们晓得应用 iris.Use 函数之后,是将中间件退出到了 APIBuilder 构造体的 middleware 切片中。那么,该 middleware 是如何和路由中的申请处理器相结合的呢?咱们还是从注册路由开始看。

    app.Get("/home",func(ctx *context.Context) {ctx.Write([]byte("Hello Wolrd"))
    })

应用 Get 函数指定一个路由。该函数的第二个参数就是对应的申请处理器,咱们称之为 handler。而后,查看 Get 的源代码,始终到 APIBuilder.handle 函数,在该函数中有创立的路由的逻辑,如下:

routes := api.createRoutes(errorCode, []string{method}, relativePath, handlers...)

在 api.createRoutes 函数的入参中,咱们只需关注 handlers,该 handlers 即是在 app.Get 中传递的 handler。持续进入 api.createRoutes 函数中,该函数是创立路由的逻辑。其实现如下:


func (api *APIBuilder) createRoutes(errorCode int, methods []string, relativePath string, handlers ...context.Handler) []*Route {
    //... 省略代码

    var (
        // global middleware to error handlers as well.
        beginHandlers = api.beginGlobalHandlers
        doneHandlers  = api.doneGlobalHandlers
    )

    if errorCode == 0 {beginHandlers = context.JoinHandlers(beginHandlers, api.middleware)
        doneHandlers = context.JoinHandlers(doneHandlers, api.doneHandlers)
    } else {beginHandlers = context.JoinHandlers(beginHandlers, api.middlewareErrorCode)
    }

    mainHandlers := context.Handlers(handlers)

    //... 省略代码
    
    routeHandlers := context.JoinHandlers(beginHandlers, mainHandlers)
    // -> done handlers
    routeHandlers = context.JoinHandlers(routeHandlers, doneHandlers)

    //... 省略代码
    routes := make([]*Route, len(methods))
    // 构建 routes 对应的 handler
    for i, m := range methods { // single, empty method for error handlers.
        route, err := NewRoute(api, errorCode, m, subdomain, path, routeHandlers, *api.macros)
        // ... 省略代码
        routes[i] = route
    }

    return routes
}

这里省略了大部分的代码,只关注和中间件及对应的申请处理器相干的逻辑。从实现上来看,能够得悉:

  • 首先看第 12 行,将全局的 beginGlobalHandlers(即 beginHandlers)和中间件 api.middleware 进行合并。这里的 api.middleware 就是咱们结尾处应用 Use 函数退出的中间件。
  • 再看第 18 行和 22 行,18 行是将路由的申请处理器转换成了切片 []Handler 切片。这里的 handlers 就是应用 Get 函数进行注册的路由。22 行是将 beginHandlers 和 mainHandlers 进行合并,能够简略的认为是将 api.middlewares 和路由注册时的申请处理器进行了合并。这里须要留神的是,通过合并申请处理器,中间件的处理器排在后面,具体的路由申请处理器排在了前面
  • 再看第 24 行,将合并后的申请处理器再和全局的 doneHandlers 进行合并。这里可暂且认为 doneHandlers 为空。

依据以上逻辑,对于一个具体的路由来说,其对应的申请处理器不仅仅是本人指定的那个,而是造成如下程序的 一组申请处理器

接下来,咱们再看在路由匹配过程中,即匹配到了具体的路由后,这一组申请处理器是如何执行的。

在 iris 中,路由匹配的过程是在文件的 /iris/core/router/handler.go 文件中的 routerHandler 构造体的 HandleRequest 函数 中执行的。如下:

func (h *routerHandler) HandleRequest(ctx *context.Context) {method := ctx.Method()
    path := ctx.Path()
    // 省略代码...

    for i := range h.trees {t := h.trees[i]

        // 省略代码...

        // 依据门路匹配具体的路由
        n := t.search(path, ctx.Params())
        if n != nil {ctx.SetCurrentRoute(n.Route)
            // 这里是找到了路由,并执行具体的申请逻辑
            ctx.Do(n.Handlers)
            // found
            return
        }
        // not found or method not allowed.
        break
    }

    ctx.StatusCode(http.StatusNotFound)
}

在匹配到路由后,会执行该路由对应的申请处理器 n.Handlers,这里的 Handlers 就是下面提到的那组蕴含中间件的申请处理器数组。咱们再来看 ctx.Do 函数的实现:

func (ctx *Context) Do(handlers Handlers) {if len(handlers) == 0 {return}

    ctx.handlers = handlers
    handlers[0](ctx)
}

这里看到在第 7 行中,首先执行第 1 个申请处理器。到这里是不是有疑难:handlers 既然是一个切片,那前面的申请处理器是如何执行的呢?这里就波及到在每个申请处理器中都有一个 ctx.Next 函数了。咱们再看下 ctx.Nex 函数的实现:

func (ctx *Context) Next() {
    // ... 省略代码
    nextIndex, n := ctx.currentHandlerIndex+1, len(ctx.handlers)
    if nextIndex < n {
        ctx.currentHandlerIndex = nextIndex
        ctx.handlers[nextIndex](ctx)
    }
}

这里咱们看第 11 行到 15 行的代码。在 ctx 中有一个以后执行到哪个 handler 的下标 currentHandlerIndex,如果还有未执行完的hander,则继续执行下一个,即ctx.handlers[nextIndex](ctx) 这也就是为什么在每个申请处理器中都应该加一行 ctx.Next 的起因。如果不加改行代码,则就执行不到后续的申请处理器

残缺的执行流程如下:

3.2 gin 中间件运行机制

因为 gin 和 iris 都是应用数组来存储中间件,所以中间件运行的机制实质上是和 iris 一样的。也是在注册路由时,将中间件的申请处理器和路由的申请处理器进行合并后作为该路由的最终的申请处理器组。在匹配到路由后,也是通过先执行申请处理器组的第一个处理器,而后调用 ctx.Next()函数进行迭代调用的。

然而,gin 的申请处理器比较简单,只有中间件和路由指定的申请处理器组成。咱们还是从路由注册指定申请处理器开始,如下

    g.GET("/", func(ctx *gin.Context){ctx.Writer.Write([]byte("Hello World"))
    })

进入 GET 的源代码,直到进入到 /gin/routergroup.go 文件中的 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()}

在该函数中咱们能够看到第 3 行处是将 group.combineHandlers(handlers),由名字可知是对申请处理器进行组合。咱们进入持续查看:

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {finalSize := len(group.Handlers) + len(handlers)
    assert1(finalSize < int(abortIndex), "too many handlers")
    mergedHandlers := make(HandlersChain, finalSize)
    copy(mergedHandlers, group.Handlers)
    copy(mergedHandlers[len(group.Handlers):], handlers)
    return mergedHandlers
}

在第 5 行,是先将 group.Handlers 即中间件退出到 mergedHandlers,而后再第 6 行再将路由具体的 handlers 退出到 mergedHandlers,最初将组合好的 mergedHandlers 作为该路由最终的 handlers。如下:

接下来,咱们再看在路由匹配过程中,即匹配到了具体的路由后,这一组申请处理器是如何执行的。

在 gin 中,路由匹配的逻辑是在 /gin/gin.go 文件的 Engine.handleHTTPRequest 函数中,如下:

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++ {
        // ... 省略代码
        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        //... 省略代码
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        // ... 省略代码
        break
    }

    // ... 省略代码
}

匹配路由以及执行对应路由解决的逻辑是在第 13 行到 18 行。在第 14 行,首先将匹配到的路由的 handlers(即中间件 + 具体的路由处理器)赋值给上下文 c,而后执行 c.Next()函数。c.Next()函数如下:

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

Next 函数 中间接就是应用下标 c.index 进行循环 handlers 的执行。这里须要留神的是 c.index 是从 - 1 开始的。所以先进行 c.index++ 则初始值就是 0。整体执行流程如下:

3.3 echo 中间件的运行机制

依据上文介绍,咱们晓得应用 echo.Use 函数来注册中间件,注册的中间件是放到了 Echo 构造体的 middleware 切片中。那么,该 middleware 是如何和路由中的申请处理器相结合的呢?咱们还是从注册路由开始看。

    e.GET("/home", func(c v4echo.Context) error {c.Response().Write([]byte("Hello World"))
        return nil
    })

应用 Get 函数指定一个路由。该函数的第二个参数就是对应的申请处理器,咱们称之为 handler。当然,在该函数中还有第三个可选的参数是针对该路由的中间件的,其原理和全局的中间件是一样的。

echo 框架的中间件和路由的处理器联合并是在路由注册的时候进行的,而是在匹配到路由后才联合的。其逻辑是在 Echo 的 ServeHTTP 函数中,如下:

func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Acquire context
    c := e.pool.Get().(*context)
    c.Reset(r, w)
    var h HandlerFunc

    if e.premiddleware == nil {e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
        h = c.Handler()
        h = applyMiddleware(h, e.middleware...)
    } else {h = func(c Context) error {e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
            h := c.Handler()
            h = applyMiddleware(h, e.middleware...)
            return h(c)
        }
        h = applyMiddleware(h, e.premiddleware...)
    }

    // Execute chain
    if err := h(c); err != nil {e.HTTPErrorHandler(err, c)
    }

    // Release context
    e.pool.Put(c)
}

在该函数的第 10 行或第 18 行。咱们接着看第 10 行中的 applyMiddleware(h, e.middleware…)函数的实现:

func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc {for i := len(middleware) - 1; i >= 0; i-- {h = middleware[i](h)
    }
    return h
}

这里的 h 是注册路由时指定的申请处理器。middelware 就是应用 Use 函数注册的所有的中间件。这里实际上循环对 h 进行层层包装。索引 i 从 middleware 切片的最初一个元素开始执行,这样就实现了先试用 Use 函数注册的中间件先执行。

这里的实现跟应用数组实现不太一样。咱们以应用 Recover 中间件为例看下具体的嵌套过程。

package main

import (
    v4echo "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {e := v4echo.New()
    // 通过 use 函数应用中间件 Recover
    e.Use(middleware.Recover())
    e.GET("/home", func(c v4echo.Context) error {c.Response().Write([]byte("Hello World"))
        return nil
    })

    e.Start(":8080")
}

这里的 Recover 中间件实际上是如下函数:

func(next echo.HandlerFunc) echo.HandlerFunc {return func(c echo.Context) error {if config.Skipper(c) {return next(c)
        }

        defer func() {// ... 省略具体逻辑代码}()
        return next(c)
    }
}

而后路由对应的申请处理器咱们假如是 h:

func(c v4echo.Context) error {c.Response().Write([]byte("Hello World"))
    return nil
}

那么,执行 applyMiddleware 函数,则后果执行了 Recover 函数,传给 Recover 函数的 next 参数的值是 h(即路由注册的申请处理器),如下:
那么新的申请处理器就变成了如下:

func(c echo.Context) error {if config.Skipper(c) {return next(c)
    }

    defer func() {// ... 省略具体逻辑代码}()
    
    return h(c) // 这里的 h 就是路由注册的申请解决
}

你看,最终还是个申请处理器的类型。这就是 echo 框架中间件的包装原理:返回一个新的申请处理器,该处理器的逻辑是 中间件的逻辑 + 输出的申请解决的逻辑。其实这个也是经典的 pipeline 模式。如下:

四、总结

本文剖析了 gin、iris 和 echo 支流框架的中间件的实现原理。其中 gin 和 iris 是通过遍历切片的形式实现的,构造也比较简单。而 echo 是通过 pipeline 模式实现的。置信通过本篇文章,你对中间件的运行原理有了更深的了解。

— 特地举荐 —

特地举荐:一个专一 go 我的项目实战、我的项目中踩坑教训及避坑指南、各种好玩的 go 工具的公众号,「Go 学堂」,专一实用性,十分值得大家关注。点击下方公众号卡片,间接关注。关注送《100 个 go 常见的谬误》pdf 文档。

退出移动版