深入了解Go語言中web框架的中間件運行機制
一、中間件的基本使用
在web開發(fā)中,中間件起著很重要的作用。比如,身份驗證、權(quán)限認證、日志記錄等。以下就是各框架對中間件的基本使用。
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函數(shù)使用中間件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函數(shù)使用中間件 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函數(shù)使用中間件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
函數(shù)來使用中間件 - 都內(nèi)置了
Recover
中間件 - 都是先執(zhí)行中間件
Recover
的邏輯,然后再輸出Hello World
接下來我們繼續(xù)分析中間件的具體實現(xiàn)。
二、中間件的實現(xiàn)
2.1 iris中間件實現(xiàn)
2.1.1 iris框架中間件類型
首先,我們看下Use函數(shù)的簽名,如下:
func (api *APIBuilder) Use(handlers ...context.Handler) { api.middleware = append(api.middleware, handlers...) }
在該函數(shù)中,handlers是一個不定長參數(shù),說明是一個數(shù)組。參數(shù)類型是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...) }
總結(jié):在iris框架上中間件也是一個請求處理器。通過Use函數(shù)使用中間件,實際上是將該中間件統(tǒng)一加入到了api.middleware切片中。該切片我們在后面再深入研究。
2.1.2 iris中自定義中間件
了解了中間件的類型,我們就可以根據(jù)其規(guī)則來定義自己的中間件了。如下:
import "github.com/kataras/iris/v12/context" func CustomMiddleware(ctx *context.Context) { fmt.Println("this is the custom middleware") // 具體的處理邏輯 ctx.Next() }
當然,為了代碼風格統(tǒng)一,也可以類似Recover中間件那樣定義個包,然后定義個New函數(shù),New函數(shù)返回的是一個中間件函數(shù),如下:
package CustomMiddleware func New() context.Handler { return func(ctx *context.Context) { fmt.Println("this is the custom middleware") // 具體的處理邏輯 ctx.Next() } }
到此為止,你有沒有發(fā)現(xiàn),無論是自定義的中間件,還是iris框架中已存在的中間件,在最后都有一行ctx.Next()代碼。那么,該為什么要有這行代碼呢? 通過函數(shù)名可以看到執(zhí)行下一個請求處理器。 再結(jié)合我們在使用Use函數(shù)使用中間件的時候,是把該中間件處理器加入到了一個切片中。所以,Next和請求處理器切片是有關(guān)系的。這個我們在下文的運行機制部分詳細解釋。
2.2 gin中間件的實現(xiàn)
2.2.1 gin框架中間件類型
同樣先查看gin的Use函數(shù)的簽名和實現(xiàn),如下:
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { engine.RouterGroup.Use(middleware...) engine.rebuild404Handlers() engine.rebuild405Handlers() return engine }
在gin框架的Use函數(shù)中,middleware
也是一個不定長的參數(shù),其參數(shù)類型是HandlerFunc
。而HandlerFunc
的定義如下:
type HandlerFunc func(*Context)
同樣,在gin框架中注冊路由時指定的請求處理器的類型也是HandlerFunc,即func(*Context)。我們再看Use中的第2行代碼engine.RouterGroup.Use(middleware...)的實現(xiàn):
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }
同樣,也是將中間件加入到了路由的Handlers切片中。
總結(jié):在gin框架中,中間件也是一個請求處理函數(shù)。通過Use函數(shù)使用中間件,實際上也是將該中間件統(tǒng)一加入到了group.Handlers切片中。
2.2.2 gin中自定義中間件
了解了gin的中間件類型,我們就可以根據(jù)其規(guī)則來定義自己的中間件了。如下:
import "github.com/gin-gonic/gin" func CustomMiddleware(ctx *gin.Context) { fmt.Println("this is gin custom middleware") // 處理邏輯 ctx.Next() }
當然,為了代碼風格統(tǒng)一,也可以類似Recover中間件那樣返回一個,然后定義個New函數(shù),New函數(shù)返回的是一個中間件函數(shù),如下:
func CustomMiddleware() gin.HandlerFunc { return func(ctx *gin.Context) { fmt.Println("this is gin custom middleware") // 處理邏輯 ctx.Next() } }
同樣,在gin的中間件中,代碼的最后一行也是ctx.Next()
函數(shù)。如果不要這行代碼行不行呢?和iris的道理是一樣的,我們也在下文的運行機制中講解。
2.3 echo框架中間件的實現(xiàn)
2.3.1 echo框架中間件類型
func (e *Echo) Use(middleware ...MiddlewareFunc) { e.middleware = append(e.middleware, middleware...) }
在echo框架中,Use函數(shù)中的middleware參數(shù)也是一個不定長參數(shù),說明可以添加多個中間件。其類型是MiddlewareFunc。如下是MiddewareFunc類型的定義:
type MiddlewareFunc func(next HandlerFunc) HandlerFunc
這個中間件的函數(shù)類型跟iris和gin的不一樣。該函數(shù)類型接收一個HandlerFunc,并返回一個HanderFunc。而HanderFunc的定義如下:
HandlerFunc func(c Context) error
HanderFunc類型才是指定路由時的請求處理器類型。我們再看下echo框架中Use的實現(xiàn),也是將middleware加入到了一個全局的切片中。
總結(jié):在echo框架中,中間件是一個輸入請求處理器,并返回一個新請求處理器的函數(shù)類型。這是和iris和gin框架不一樣的地方。通過Use函數(shù)使用中間件,也是將該中間件統(tǒng)一加入到全局的中間件切片中。
2.3.2 echo中自定義中間件
了解了echo的中間件類型,我們就可以根據(jù)其規(guī)則來定義自己的中間件了。如下:
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) } }
這里中間件的實現(xiàn)看起來比較復雜,做下簡單的解釋。根據(jù)上面可知,echo的中間件類型是輸入一個請求處理器,然后返回一個新的請求處理器。在該函數(shù)中,從第6行到第10行該函數(shù)其實是中間件的執(zhí)行邏輯。第9行的next(c)實際上是要執(zhí)行下一個請求處理器的邏輯,類似于iris和gin中的ctx.Next()函數(shù)。** 本質(zhì)上是用一個新的請求處理器(返回的請求處理器)包裝了一下舊的請求處理器(輸入的next請求處理器)**。
中間件的定義和使用都介紹了。那么,中間件和具體路由中的請求處理器是如何協(xié)同工作的呢?下面我們介紹中間件的運行機制。
三、中間件的運行機制
3.1 iris中間件的運行機制
根據(jù)上文介紹,我們知道使用iris.Use函數(shù)之后,是將中間件加入到了APIBuilder結(jié)構(gòu)體的middleware切片中。那么,該middleware是如何和路由中的請求處理器相結(jié)合的呢?我們還是從注冊路由開始看。
app.Get("/home",func(ctx *context.Context) { ctx.Write([]byte("Hello Wolrd")) })
使用Get函數(shù)指定一個路由。該函數(shù)的第二個參數(shù)就是對應的請求處理器,我們稱之為handler。然后,查看Get的源代碼,一直到APIBuilder.handle函數(shù),在該函數(shù)中有創(chuàng)建的路由的邏輯,如下:
routes := api.createRoutes(errorCode, []string{method}, relativePath, handlers...)
在api.createRoutes函數(shù)的入?yún)⒅校覀冎恍桕P(guān)注handlers,該handlers即是在app.Get中傳遞的handler。繼續(xù)進入api.createRoutes函數(shù)中,該函數(shù)是創(chuàng)建路由的邏輯。其實現(xiàn)如下:
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)) // 構(gòu)建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 }
這里省略了大部分的代碼,只關(guān)注和中間件及對應的請求處理器相關(guān)的邏輯。從實現(xiàn)上來看,可以得知:
- 首先看第12行,將全局的beginGlobalHandlers(即beginHandlers)和中間件api.middleware進行合并。這里的api.middleware就是我們開頭處使用Use函數(shù)加入的中間件。
- 再看第18行和22行,18行是將路由的請求處理器轉(zhuǎn)換成了切片 []Handler切片。這里的handlers就是使用Get函數(shù)進行注冊的路由。22行是將beginHandlers和mainHandlers進行合并,可以簡單的認為是將api.middlewares和路由注冊時的請求處理器進行了合并。這里需要注意的是,通過合并請求處理器,中間件的處理器排在前面,具體的路由請求處理器排在了后面。
- 再看第24行,將合并后的請求處理器再和全局的doneHandlers進行合并。這里可暫且認為doneHandlers為空。
根據(jù)以上邏輯,對于一個具體的路由來說,其對應的請求處理器不僅僅是自己指定的那個,而是形成如下順序的一組請求處理器:
接下來,我們再看在路由匹配過程中,即匹配到了具體的路由后,這一組請求處理器是如何執(zhí)行的。
在iris中,路由匹配的過程是在文件的/iris/core/router/handler.go
文件中的routerHandler
結(jié)構(gòu)體的HandleRequest函數(shù)
中執(zhí)行的。如下:
func (h *routerHandler) HandleRequest(ctx *context.Context) { method := ctx.Method() path := ctx.Path() // 省略代碼... for i := range h.trees { t := h.trees[i] // 省略代碼... // 根據(jù)路徑匹配具體的路由 n := t.search(path, ctx.Params()) if n != nil { ctx.SetCurrentRoute(n.Route) // 這里是找到了路由,并執(zhí)行具體的請求邏輯 ctx.Do(n.Handlers) // found return } // not found or method not allowed. break } ctx.StatusCode(http.StatusNotFound) }
在匹配到路由后,會執(zhí)行該路由對應的請求處理器n.Handlers,這里的Handlers就是上面提到的那組包含中間件的請求處理器數(shù)組。我們再來看ctx.Do函數(shù)的實現(xiàn):
func (ctx *Context) Do(handlers Handlers) { if len(handlers) == 0 { return } ctx.handlers = handlers handlers[0](ctx) }
這里看到在第7行中,首先執(zhí)行第1個請求處理器。到這里是不是有疑問:handlers既然是一個切片,那后面的請求處理器是如何執(zhí)行的呢?這里就涉及到在每個請求處理器中都有一個ctx.Next函數(shù)了。我們再看下ctx.Nex函數(shù)的實現(xiàn):
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
中有一個當前執(zhí)行到哪個handler的下標currentHandlerIndex
,如果還有未執(zhí)行完的hander
,則繼續(xù)執(zhí)行下一個,即ctx.handlers[nextIndex](ctx)
。這也就是為什么在每個請求處理器中都應該加一行ctx.Next的原因。如果不加改行代碼,則就執(zhí)行不到后續(xù)的請求處理器。
完整的執(zhí)行流程如下:
3.2 gin中間件運行機制
由于gin和iris都是使用數(shù)組來存儲中間件,所以中間件運行的機制本質(zhì)上是和iris一樣的。也是在注冊路由時,將中間件的請求處理器和路由的請求處理器進行合并后作為該路由的最終的請求處理器組。在匹配到路由后,也是通過先執(zhí)行請求處理器組的第一個處理器,然后調(diào)用ctx.Next()函數(shù)進行迭代調(diào)用的。
但是,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() }
在該函數(shù)中我們可以看到第3行處是將group.combineHandlers(handlers),由名字可知是對請求處理器進行組合。我們進入繼續(xù)查看:
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。如下:
接下來,我們再看在路由匹配過程中,即匹配到了具體的路由后,這一組請求處理器是如何執(zhí)行的。
在gin中,路由匹配的邏輯是在/gin/gin.go文件的Engine.handleHTTPRequest函數(shù)中,如下:
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 } // ...省略代碼 }
匹配路由以及執(zhí)行對應路由處理的邏輯是在第13行到18行。在第14行,首先將匹配到的路由的handlers(即中間件+具體的路由處理器)賦值給上下文c,然后執(zhí)行c.Next()函數(shù)。c.Next()函數(shù)如下:
func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
在Next函數(shù)
中直接就是使用下標c.index
進行循環(huán)handlers
的執(zhí)行。這里需要注意的是c.index
是從-1開始的。所以先進行c.index++
則初始值就是0。整體執(zhí)行流程如下:
3.3 echo中間件的運行機制
根據(jù)上文介紹,我們知道使用echo.Use函數(shù)來注冊中間件,注冊的中間件是放到了Echo結(jié)構(gòu)體的middleware切片中。那么,該middleware是如何和路由中的請求處理器相結(jié)合的呢?我們還是從注冊路由開始看。
e.GET("/home", func(c v4echo.Context) error { c.Response().Write([]byte("Hello World")) return nil })
使用Get函數(shù)指定一個路由。該函數(shù)的第二個參數(shù)就是對應的請求處理器,我們稱之為handler。當然,在該函數(shù)中還有第三個可選的參數(shù)是針對該路由的中間件的,其原理和全局的中間件是一樣的。
echo框架的中間件和路由的處理器結(jié)合并是在路由注冊的時候進行的,而是在匹配到路由后才結(jié)合的。其邏輯是在Echo的ServeHTTP函數(shù)中,如下:
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) }
在該函數(shù)的第10行或第18行。我們接著看第10行中的applyMiddleware(h, e.middleware...)函數(shù)的實現(xiàn):
func applyMiddleware(h HandlerFunc, middleware ...MiddlewareFunc) HandlerFunc { for i := len(middleware) - 1; i >= 0; i-- { h = middleware[i](h) } return h }
這里的h是注冊路由時指定的請求處理器。middelware就是使用Use函數(shù)注冊的所有的中間件。這里實際上循環(huán)對h進行層層包裝。 索引i從middleware切片的最后一個元素開始執(zhí)行,這樣就實現(xiàn)了先試用Use函數(shù)注冊的中間件先執(zhí)行。
這里的實現(xiàn)跟使用數(shù)組實現(xiàn)不太一樣。我們以使用Recover中間件為例看下具體的嵌套過程。
package main import ( v4echo "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) func main() { e := v4echo.New() // 通過use函數(shù)使用中間件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中間件實際上是如下函數(shù):
func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { if config.Skipper(c) { return next(c) } defer func() { // ...省略具體邏輯代碼 }() return next(c) } }
然后路由對應的請求處理器我們假設(shè)是h:
func(c v4echo.Context) error { c.Response().Write([]byte("Hello World")) return nil }
那么,執(zhí)行applyMiddleware函數(shù),則結(jié)果執(zhí)行了Recover函數(shù),傳給Recover函數(shù)的next參數(shù)的值是h(即路由注冊的請求處理器),如下: 那么新的請求處理器就變成了如下:
func(c echo.Context) error { if config.Skipper(c) { return next(c) } defer func() { // ...省略具體邏輯代碼 }() return h(c) // 這里的h就是路由注冊的請求處理 }
你看,最終還是個請求處理器的類型。這就是echo框架中間件的包裝原理:返回一個新的請求處理器,該處理器的邏輯是 中間件的邏輯 + 輸入的請求處理的邏輯。其實這個也是經(jīng)典的pipeline模式。如下:
四、總結(jié)
本文分析了gin、iris和echo主流框架的中間件的實現(xiàn)原理。其中g(shù)in和iris是通過遍歷切片的方式實現(xiàn)的,結(jié)構(gòu)也比較簡單。而echo是通過pipeline模式實現(xiàn)的。相信通過本篇文章,你對中間件的運行原理有了更深的理解。
以上就是深入了解Go語言中web框架的中間件運行機制的詳細內(nèi)容,更多關(guān)于Go語言web框架中間件運行機制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
GoLang中的互斥鎖Mutex和讀寫鎖RWMutex使用教程
RWMutex是一個讀/寫互斥鎖,在某一時刻只能由任意數(shù)量的reader持有或者一個writer持有。也就是說,要么放行任意數(shù)量的reader,多個reader可以并行讀;要么放行一個writer,多個writer需要串行寫2023-01-01