用 20 行代碼寫出清晰易用的 Go 中間件 API
在使用 Go 編寫復雜的服務時,您將遇到一個典型的主題是中間件。這個話題在網上被討論了一次又一次。本質上,中間件允許我們做了如下事情:
- 攔截 ServeHTTP 調用,執行任意代碼
- 對調用鏈(Continuation Chain) 上的請求/響應流進行更改
- 打斷中間件鏈,或繼續下一個中間件攔截器并最終到達真正的請求處理器
這些與 express.js 中間件所做的工作非常類似。我們探索了各種庫,找到了接近我們想要的現有解決方案,但是他們要么有不要的額外內容,要么不符合我們的品位。顯然,我們可以在 express.js 中間件的啟發下,寫出 20 行代碼以下的更清晰的易用的 API(Installation API)
抽象
在設計抽象時,我們首先設想如何編寫中間件函數(下文開始稱為攔截器),答案非常明顯:
- func NewElapsedTimeInterceptor() MiddlewareInterceptor {
- return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- startTime := time.Now()
- defer func() {
- endTime := time.Now()
- elapsed := endTime.Sub(startTime)
- // 記錄時間消耗
- }()
- next(w, r)
- }
- }
- func NewRequestIdInterceptor() MiddlewareInterceptor {
- return func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
- if r.Headers.Get("X-Request-Id") == "" {
- r.Headers.Set("X-Request-Id", generateRequestId())
- }
- next(w, r)
- }
- }
它們看起來就像 http.HandlerFunc,但有一個額外的參數 next,該函數(參數)會繼續處理請求鏈。這將允許任何人像編寫類似 http.HandlerFunc 的簡單函數一樣寫攔截器,它可以攔截調用,執行所需操作,并在需要時傳遞控制權。
接下來,我們設想如何將這些攔截器連接到 http.Handler 或 http.HandlerFunc 中。為此,首先要定義 MiddlewareHandlerFunc,它只是 http.HandlerFunc 的一種類型。(type MiddlewareHandlerFunc http.HandlerFunc)。這將允許我們在 http.HandlerFunc 棧上之上構建一個更好的 API。現在給定一個 http.HandlerFunc 我們希望我們的鏈式 API 看起來像這樣:
- func HomeRouter(w http.ResponseWriter, r *http.Request) {
- // 處理請求
- }
- // ...
- // 在程序某處注冊 Hanlder
- chain := MiddlewareHandlerFunc(HomeRouter).
- Intercept(NewElapsedTimeInterceptor()).
- Intercept(NewRequestIdInterceptor())
- // 像普通般注冊 HttpHandler
- mux.Path("/home").HandlerFunc(http.HandlerFunc(chain))
將 http.HandlerFunc 傳遞到 MiddlewareHandlerFunc,然后調用 Intercept 方法注冊我們的 Interceptor。Interceptor 的返回類型還是 MiddlewareHandlerFunc,它允許我們再次調用 Intercept。
使用 Intercept 組合需要注意的一件重要事情是執行的順序。由于 chain(responseWriter, request)是間接調用最后一個攔截器,攔截器的執行是反向的,即它從尾部的攔截器一直返回到頭部的處理程序。這很有道理,因為你在攔截調用時,攔截器應該要在真正的請求處理器之前執行。
簡化
雖然這種反向鏈系統使抽象更加流暢,但事實證明,大多數情況下 s 我們有一個預編譯的攔截器數組,能夠在不同的 handlers 之間重用。同樣,當我們將中間件鏈定義為數組時,我們自然更愿意以它們執行順序聲明它們(而不是相反的順序)。讓我們將這個數組攔截器稱為中間件鏈。我們希望我們的中間件鏈看起來有點像:
- // 調用鏈或中間件可以按下標的順序執行
- middlewareChain := MiddlewareChain{
- NewRequestIdInterceptor(),
- NewElapsedTimeInterceptor(),
- }
- // 調用所有以 HomeRouter 結尾的中間件
- mux.Path("/home").Handler(middlewareChain.Handler(HomeRouter))
實現
一旦我們設計好抽象的概念,實現就顯得簡單多了
- package middleware
- import "net/http"
- // MiddlewareInterceptor intercepts an HTTP handler invocation, it is passed both response writer and request
- // which after interception can be passed onto the handler function.
- type MiddlewareInterceptor func(http.ResponseWriter, *http.Request, http.HandlerFunc)
- // MiddlewareHandlerFunc builds on top of http.HandlerFunc, and exposes API to intercept with MiddlewareInterceptor.
- // This allows building complex long chains without complicated struct manipulation
- type MiddlewareHandlerFunc http.HandlerFunc
- // Intercept returns back a continuation that will call install middleware to intercept
- // the continuation call.
- func (cont MiddlewareHandlerFunc) Intercept(mw MiddlewareInterceptor) MiddlewareHandlerFunc {
- return func(writer http.ResponseWriter, request *http.Request) {
- mw(writer, request, http.HandlerFunc(cont))
- }
- }
- // MiddlewareChain is a collection of interceptors that will be invoked in there index order
- type MiddlewareChain []MiddlewareInterceptor
- // Handler allows hooking multiple middleware in single call.
- func (chain MiddlewareChain) Handler(handler http.HandlerFunc) http.Handler {
- curr := MiddlewareHandlerFunc(handler)
- for i := len(chain) - 1; i >= 0; i-- {
- mw := chain[i]
- curr = curr.Intercept(mw)
- }
- return http.HandlerFunc(curr)
- }
因此,在不到 20 行代碼(不包括注釋)的情況下,我們就能夠構建一個很好的中間件庫。它幾乎是簡簡單單的,但是這幾行連貫的抽象實在是太棒了。它使我們能夠毫不費力地編寫一些漂亮的中間件鏈。希望這幾行代碼也能激發您的中間件體驗。