深入瞭解Go語言中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 < 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語言中web框架的中間件運行機制的詳細內容,更多關於Go語言web框架中間件運行機制的資料請關註WalkonNet其它相關文章!

推薦閱讀: