Golang Web入門(3):如何優雅的設計中間件

摘要

上一篇文章中,咱們已經能夠實現一個性能較高,且支持RESTful風格的路由了。可是,在Web應用的開發中,咱們還須要一些能夠被擴展的功能。git

所以,在設計框架的過程當中,應該留出能夠擴展的空間,好比:日誌記錄、故障恢復等功能,若是咱們把這些業務邏輯全都塞進Controller/Handler中,會顯得代碼特別的冗餘,雜亂。github

因此在這篇文章中,咱們來探究如何更優雅的設計這些中間件。數組

1 耦合的實現方式

好比咱們要實現一個日誌記錄的功能,咱們能夠用這種簡單粗暴的方式:bash

package main

import (
	"fmt"
	"net/http"
	"time"
)

func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
	record(r.URL.Path)
	fmt.Fprintf(w, "Hello World !")
}

func main() {
	http.HandleFunc("/hello", helloWorldHandler)
	http.ListenAndServe(":8000", nil)
}

func record(path string)  {
	fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + path)
}
複製代碼

若是這樣作的話,確實是實現了咱們的目標,記錄了訪問的日誌。app

可是,這樣一點都不優雅。框架

每個Handler內部都須要調用record函數,而後再把須要記錄的path做爲參數傳進record函數中。函數

若是這樣作,無論咱們須要添加什麼樣的額外功能,都必須得把這個額外的功能和咱們的業務邏輯緊緊地綁定到一塊兒,不能實現擴展功能與業務邏輯間的解耦。post

2 將記錄與實現解耦

既然在上面的實現中,記錄日誌和業務實現徹底的耦合在了一塊兒,那麼咱們能不能把他們的業務實現解耦開來呢?性能

來看這段代碼:ui

func record(w http.ResponseWriter, r *http.Request)  {
	path := r.URL.Path
	method := r.Method
	fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}

func helloWorldHandler(w http.ResponseWriter, r *http.Request) {
	record(w ,r)
	fmt.Fprintf(w, "Hello World !")
}
複製代碼

在這裏,咱們已經把業務實現和日誌記錄的耦合給解開了一部分。

咱們只須要在業務代碼中,調用record(w,r)函數,把請求的內容做爲參數傳進record函數中,而後在record這個方法內記錄日誌。這個時候,咱們能夠在方法內部任意的處理請求,保存如請求路徑、請求方法等數據。而這個過程,對業務實現是透明的

這樣作的話,咱們只須要在處理業務邏輯的Handler中調用函數,而後把參數傳進去。而這個函數的具體實現,則是與業務邏輯無關的。

那麼,有沒有辦法能夠把業務邏輯和擴展功能徹底分開,讓業務代碼裏只有業務代碼,使代碼變得更加整潔呢?咱們接着往下看。

3 設計中間件

咱們在上一篇文章裏面,分析了httprouter這個包的實現。因此咱們直接對他動手,修改他的代碼,增長一個AddBeforeHandle方法,使得這個路由具備擴展性。

注意,這裏的AddBeforeHandle方法是做者本身編寫的,具體的實現過程能夠看後文。

3.1 效果

在此以前,咱們來看看效果:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/julienschmidt/httprouter"
)

func Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Hello World!\n")
}

func record(w http.ResponseWriter, r *http.Request){
	path := r.URL.Path
	method := r.Method
	fmt.Println(time.Now().Format("3:04:05 PM Mon Jan") + " " + method + " " + path)
}


func main() {
	router := httprouter.New()
	router.AddBeforeHandle(record)
	router.GET("/hello", Hello)
	log.Fatal(http.ListenAndServe(":8080", router))
}
複製代碼

這部分的代碼和上一篇的幾乎徹底同樣。也是建立一個路由,將/hello這個路徑和Hello這個處理器綁定在GET的這顆前綴樹中,而後開始監聽8080端口。

這裏比較重要的是main方法裏面的第二行:

router.AddBeforeHandle(record)
複製代碼

從方法名能夠看出,這個方法是在Handle以前增長了一個處理過程。

再看看參數,就是咱們上面提到的記錄訪問日誌的方法,這個方法記錄了請求的URL,請求的方法,以及時間。

而在咱們的Hello(w http.ResponseWriter, r *http.Request, _ httprouter.Params)函數中,已經不包含任何其餘的業務邏輯了。

此時,這個Handler專一於處理業務邏輯,至於別的,交給別的函數去實現。這樣,就實現了徹底的解耦

下面咱們來看看具體的實現過程:

3.2 具體實現

先來看看AddBeforeHandle這個方法:

func (r *Router) AddBeforeHandle(fn func(w http.ResponseWriter, req *http.Request))  {
	r.beforeHandler = fn
}
複製代碼

這個方法很簡單,也就是接收一個處理器類型的參數,而後賦值給Router中的字段beforeHandler

這個名爲beforeHandler字段也是咱們新增在Router中的,相信你也能看得出來了,所謂的AddBeforeHandle方法,就是把咱們傳進去的處理函數,保存在Router中,在須要的時候調用他。

那麼咱們來看看,何時會調用這個方法。下面列出的這個方法,在上一篇文章有提到,是關於httprouter是如何處理路由的:

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
	if root := r.trees[req.Method]; root != nil {
		if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
		    if r.beforeHandler != nil{
				r.beforeHandler(w, req)
			}
			if ps != nil {
				handle(w, req, *ps)
				r.putParams(ps)
			} else {
				handle(w, req, nil)
			}
			return
		} 
	}
    ...
}
複製代碼

注意看,router在找到了Handler,準備執行以前,咱們添加了這麼幾行:

if r.beforeHandler != nil{
	r.beforeHandler(w, req)
}
複製代碼

也就是說,若是咱們以前調用了AddBeforeHandle方法,給beforeHandler這個字段賦了值,那麼他就不會爲nil,而後調用這個函數。這也就實現了咱們的目的,在處理請求以前,先執行咱們設置的函數。

3.3 思考

如今咱們已經實現了一個徹底解耦的中間件。而且,這個中間件是能夠任意配置的。你能夠拿來作日誌記錄,也能夠作權限校驗等等,並且這些功能還不會對Handler中的業務邏輯形成影響。

若是你是個Java開發者,你可能會以爲這個很像Filter,或者是AOP

可是,和過濾器不一樣的是,咱們不只能夠在請求到來以前處理,也能夠在請求完成以後處理。好比這個請求發生了一些panic,你能夠在最後處理它,或者你能夠記錄這個請求的時間等等,你要作的,只是在Handle方法以後,調用你所註冊的方法。

好比:

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	...
	if root := r.trees[req.Method]; root != nil {
		if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
		    if r.beforeHandler != nil{
				r.beforeHandler(w, req)
			}
			if ps != nil {
				handle(w, req, *ps)
				r.putParams(ps)
			} else {
				handle(w, req, nil)
			}
			if r.afterHandler != nil {
				r.afterHandler(w, req)
			}
			return
		} 
	}
    ...
}
複製代碼

咱們只是添加了一個afterHandler方法,就是這麼的簡單。

那麼問題來了:如今這樣的處理操做,咱們僅僅只能在請求前和請求後各自添加一箇中間件。若是咱們想要添加任意多箇中間件,該怎麼作呢?

能夠先本身思考一下,而後咱們來看看在gin中,是怎麼實現的。

4 Gin的中間件

4.1 使用

衆所周知,在閱讀源碼以前,必定要先看看他是怎麼用的:

package main

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

func Hello(ctx *gin.Context) {
	fmt.Fprint(ctx.Writer, "Hello World!\n")
}

func main() {
	router := gin.New()
	router.Use(gin.Logger(), gin.Recovery())
	router.GET("/hello", Hello)
	router.Run(":8080")
}
複製代碼

能夠看到,在gin中,使用中間件的方法和上文中咱們所設計的是差很少的。都是業務和中間件徹底解耦,而且在註冊路由的時候,添加進去。

可是咱們注意到,在gin中是不分Handle以前仍是Handle以後的。那麼他是如何作到的呢,咱們來看看源碼。

4.2 源碼解釋

先從Use方法看起:

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

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

在這裏咱們先無論group這個東西,他是路由分組,和咱們這篇文章沒有關係,咱們先無論他。咱們只須要看到append方法。Use方法就是把參數裏面的函數,所有增長到group.Handlers中。這裏的group.Handlers,是一個Handler類型的數組。

因此,在gin中,每個中間件,也是Handler類型的。

在上一節咱們留了一個問題,要怎麼實現多箇中間件。答案就在這裏了,用數組保存。

那麼問題又來了:怎麼保證調用的順序呢?

咱們繼續往下看看路由的註冊:

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}
複製代碼

這裏是否是也有點熟悉呢?和上一篇文章提到的httprouter很類似,咱們直接看group.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()
}
複製代碼

在這段代碼中,第一行關於path的咱們先無論,這個也是和路由分組有關的,簡單來講就是拼接出完整的請求path

先看看第二行,方法名是combineHandlers,咱們能夠猜想一下這個方法的做用,把各個Handler結合起來。看看詳細的代碼:

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
}
複製代碼

先解釋一下,這裏返回的HandlersChain類型,是Handler的數組。

也就是說,在這個方法裏面,把以前放入group中的中間件,和當前路由的Handler,組合成一個新的數組。

而且,中間件在前面,路由Handler在後面。注意,這個順序很重要

而後咱們繼續往下,執行完這個方法以後執行的就是addRoute方法了。在這裏不展開講。因此最重要的是,這裏把中間件和Handler全都組合在了一塊兒,綁定到了這個前綴樹上。

到了這裏註冊方面的內容已經結束了,咱們來看看他是怎麼處理各個中間件的調用順序

由於咱們的目的是看路由是怎麼處理請求的,因此咱們直接看ginServeHTTP方法:

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()

	engine.handleHTTPRequest(c)

	engine.pool.Put(c)
}
複製代碼

這裏要注意的是*Context,他是對請求的封裝,包含了有responseWriter*http.Request等。

咱們繼續往下看看handleHTTPRequest(c)這個方法:

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++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		value := root.getValue(rPath, c.Params, unescape)
		if value.handlers != nil {
			c.handlers = value.handlers
			c.Params = value.params
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
		...
	}
	...
}
複製代碼

在這個方法中,其實和以前咱們研究的httprouter是很類似的。也是先根據請求方法找到相對應的前綴樹,而後獲取相對應的Handler,並把獲取到的handler數組保存在Context中。

這裏咱們注意看c.Next()方法,他是gin中關於中間件的調用最精妙的部分。咱們來看看:

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

咱們能夠看到,當調用這個Next()方法的時候,會增長保存在Context中的下標,而後根據這個下標的順序執行handler

而在前面咱們有提到,咱們把中間件排在了這個handler數組的前面,先執行中間件,而後最後纔是執行用戶自定義的handler

咱們再來看看日誌記錄這個中間件:

func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
	...
	return func(c *Context) {
		//開始計時
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		c.Next()
		...
		// Stop timer
		param.TimeStamp = time.Now()
		param.Latency = param.TimeStamp.Sub(start)
		...
	}
}
複製代碼

能夠看到,先開始計時,而後調用了c.Next()這個方法,而後才結束計時。

那麼咱們能夠由此推斷,c.Next()後面的代碼,是執行完用戶自定義的Handler才執行的。

也就是說,其實中間件的業務邏輯是這樣的:

func Middleware(c *gin.Context){
    //請求前執行
    c.Next()
    //請求後執行
}
複製代碼

5 寫在最後

首先,謝謝你能看到這裏。

簡單的來說,咱們應該考慮解耦合,使得業務代碼能夠專一於業務,中間件專一於實現功能。爲了實現這點,咱們能夠修改路由的實現邏輯,在執行Handler的先後加入中間件的調用。

在本文中,可能會有不少的疏漏。若是在閱讀的過程當中,有哪些解釋不到位,或者做者的理解出現了一些差錯,也請你留言指正。

再次感謝~

PS:若是有其餘的問題,也能夠在公衆號找到做者。而且,全部文章第一時間會在公衆號更新,歡迎來找做者玩~

相關文章
相關標籤/搜索