什么是中間件?
在網絡世界中,中間件針對客戶端發起的網絡請求而執行的代碼,並且將請求鏈接到下一個中間件,最終到達目標處理函數。當您希望對多個不同請求做一些共享的操作,中間件是很有用的。例如對網絡請求進行身份驗證、性能日志記錄和數據收集。
舉一個例子,假如我們有一個服務返回用戶提交的消息列表,另一個服務是返回關於該用戶的個人敏感信息。只有當用戶登錄到服務后,才能訪問這兩個服務。此外,我們還希望記錄客戶端向服務器發出的每個請求的執行時間。身份驗證和日志記錄的代碼可以與控制器一起編寫,並復制到兩個服務即可。但是這個例子很容易被分成四個不同的包來管理。一個記錄請求數據(LOG),一個驗證用戶是否登錄(AUTH),一個返回消息列表(RETURN MSG),和一個返回用戶數據(RETURN USER)。
通過將這些功能作為中間件來編寫,我們可以很容易將請求連接起來,並在需要時在每個中間件上提前返回。例如,用戶沒有登錄就發送請求。整個處理過程可能是如下方式:
[LOG] -> [AUTH] -> [RETURN MSG]
[LOG] -> [AUTH] -> [RETURN USER]
由於分離了LOG和AUTH,我們只需要編寫代碼和測試一次就可以。
Go中間件
在Go中,所有的網絡請求都通過實現net/http包中Handler接口來完成。對不太熟悉Go的開發人員來說,Handler是響應請求並處理對請求參數的讀取和響應的寫入。因為Go在其關鍵函數中都需要這個接口,所以圍繞它構建包或者裝飾器來擴展接口是很容易並且可靠的。
創建中間件的快速方法就是封裝net/http.Handler.ServeHTTP方法:
ServeHTTP(ResponseWriter, *Request)
通過添加一個外部函數,該函數接收一個Handler類型參數,並使用HandlerFunc函數來返回一個Handler:
func middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Code for the middleware... next.ServeHTTP(w, r) }) }
上面的函數接收一個Handler類型的參數,該參數將請求鏈接到最終的函數上去。因為返回值也是Handler類型,可以將調用串起來:
firstMiddleware(secondMiddleware(lastHandler))
僅通過這個簡單的方法,就可以很好地實現中間件。雖然不是很健壯,但是可行的。
接下來,我將使用Negroni這個包,使創建中間件變得更簡單。當然你也可以自己隨意的實現。Negroni封裝了router,並為中間件的管理提供了功能。比如更簡單,靈活地連接中間件。
實現-對Handler的封裝
為了盡可能的簡單,下面實現一個中間件來記錄網絡請求的執行時間。將使用Negroni來實現,先創建router然后用Negroni來封裝:
package main import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/urfave/negroni" ) func main() { router := mux.NewRouter() router. Methods("GET"). Path("/"). HandlerFunc(endpointHandler) n := negroni.New() n.UseHandler(router) err := http.ListenAndServe(":8080", n) if err != nil { panic(err) } } func endpointHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Endpoint handler called") }
如上面代碼所示,先用gorilla/mux包創建router,然后注冊一個Get服務。並且創建一個Negroni實例,將router作為參數傳入UserHanler函數。因為Negroni實現了Handler接口,可以像使用router那樣來使用它。運行以上代碼並發起Get請求(curl localhost:8080/ ),可以觀察到“Endpoint handler called”內容返回給客戶端。
中間件的實現
下面繼續創建中間件,如前所述,中間件就是一個Handler。因此,我們需要做的就是創建一個結構體來實現Handler接口。這里我們使用Negroni,我們只需要實現它的Handler接口即可。
如下所示,和net/http.Hanler接口類似,除了它還支持與下一個參數鏈接:
type Handler interface { ServeHTTP( rw http.ResponseWriter, r *http.Request, next http.HandlerFunc, ) }
讓我們創建一個簡單的打印日志程序:
package mw import ( "fmt" "net/http" "time" ) type Logger struct{} func (*Logger) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { fmt.Println("The logger middleware is executing!") t := time.Now() next.ServeHTTP(w, r) fmt.Printf("Execution time: %s \n", time.Now().Sub(t).String()) }
上面的代碼展示了如何通過實現Negroni.Handler來創建一個簡單的中間件。對請求的執行時間進行記錄。最后,繼續執行下一個Handler,它可能是另一個中間件或是最終的處理函數。在該調用完成執行后,記錄總的執行時間。
將上面日志中間件添加到router上,看看執行時間:
package main import ( "fmt" "net/http" "github.com/gorilla/mux" "github.com/johan-lejdung/go-microservice-middleware-guide/mw" "github.com/urfave/negroni" ) func main() { router := mux.NewRouter() router. Methods("GET"). Path("/"). HandlerFunc(endpointHandler) n := negroni.New() n.Use(&mw.Logger{}) n.UseHandler(router) err := http.ListenAndServe(":8080", n) if err != nil { panic(err) } } func endpointHandler(w http.ResponseWriter, r *http.Request) { fmt.Println("Endpoint handler called") }
我們只是使用n.Use(&mw.Logger{}),告訴Negroni調用創建的Logger中間件,中間件會按照添加順序執行。執行上面的代碼就會得到如下日志:
The logger middleware is executing! Endpoint handler called Execution time: 25.146µs
第一行和第三行是從中間件生成的,而第二行是在函數endpoint Handler中生成的。我們成功地實現了中間件。
讓我們考慮一下,我們還有一個身份驗證中間件(AuthMW),這個AuthMW將確保未經授權的請求無法到達最終執行函數。要將它添加到鏈中,我們只需執行以下操作:
n.Use(&mw.Logger{})
n.Use(&mw.Auth{})
