深入了解Go语言中web框架的中间件运行机制
本篇文章向大家介绍《深入了解Go语言中web框架的中间件运行机制》,主要包括语言web框架,具有一定的参考价值,需要的朋友可以参考一下。
一、中间件的基本使用
在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 这里我们看第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 在第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 匹配路由以及执行对应路由处理的逻辑是在第13行到18行。在第14行,首先将匹配到的路由的handlers(即中间件+具体的路由处理器)赋值给上下文c,然后执行c.Next()函数。c.Next()函数如下:
func (c *Context) Next() {
c.index++
for 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模式实现的。相信通过本篇文章,你对中间件的运行原理有了更深的理解。
今天带大家了解了语言web框架的相关知识,希望对你有所帮助;关于Golang的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~
GoLang函数栈的使用详细讲解
- 上一篇
- GoLang函数栈的使用详细讲解
- 下一篇
- MySQL多表操作的外键约束教程
-
- Golang · Go教程 | 6小时前 | 格式化输出 printf fmt库 格式化动词 Stringer接口
- Golangfmt库用法与格式化技巧解析
- 140浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Golang配置Protobuf安装教程
- 147浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Golang中介者模式实现与通信解耦技巧
- 378浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Golang多协程通信技巧分享
- 255浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Golang如何判断变量类型?
- 393浏览 收藏
-
- Golang · Go教程 | 7小时前 |
- Golang云原生微服务实战教程
- 310浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang迭代器与懒加载结合应用
- 110浏览 收藏
-
- Golang · Go教程 | 8小时前 | 性能优化 并发安全 Golangslicemap 预设容量 指针拷贝
- Golangslicemap优化技巧分享
- 412浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang代理模式与访问控制实现解析
- 423浏览 收藏
-
- Golang · Go教程 | 8小时前 |
- Golang事件管理模块实现教程
- 274浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3166次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3379次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3408次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4512次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3788次使用
-
- Golangmap实践及实现原理解析
- 2022-12-28 505浏览
-
- go和golang的区别解析:帮你选择合适的编程语言
- 2023-12-29 503浏览
-
- 试了下Golang实现try catch的方法
- 2022-12-27 502浏览
-
- 如何在go语言中实现高并发的服务器架构
- 2023-08-27 502浏览
-
- 提升工作效率的Go语言项目开发经验分享
- 2023-11-03 502浏览

