十分鐘學會用Go編寫Web中間件

  • 2020 年 2 月 17 日
  • 筆記

中間件(通常)是一小段代碼,它們接收一個請求,對其進行處理,每個中間件只處理一件事情,完成後將其傳遞給另一個中間件或最終處理程序,這樣就做到了程序的解耦。如果沒有中間件那麼我們必須在最終的處理程序中來完成這些處理操作,這無疑會造成處理程序的臃腫和代碼復用率不高的問題。中間件的一些常見用例是請求日誌記錄, Header操縱、 HTTP請求認證和 ResponseWriter劫持等等。

畫外音:上面這段描述中間件的文字,跟我很早前在Laravel源碼解析之中間件寫的幾乎一樣(其實這圖也是從那裡拿過來的)。再次說明做開發時間長了以後掌握一些編程的思想有時候比掌握一門編程語言更重要,這不咱們就又用Go來寫中間件了。

創建中間件

接下來我們用 Go創建中間件,中間件只將 http.HandlerFunc作為其參數,在中間件里將其包裝並返回新的 http.HandlerFunc供服務器服務復用器調用。這裡我們創建一個新的類型 Middleware,這會讓最後一起鏈式調用多個中間件變的更簡單。

type Middleware func(http.HandlerFunc) http.HandlerFunc

下面的中間件通用代碼模板讓我們平時編寫中間件變得更容易。

中間件代碼模板

中間件是使用裝飾器模式實現的,下面的中間件通用代碼模板讓我們平時編寫中間件變得更容易,我們在自己寫中間件的時候只需要往樣板里填充需要的代碼邏輯即可。

func createNewMiddleware() Middleware {      // 創建一個新的中間件      middleware := func(next http.HandlerFunc) http.HandlerFunc {          // 創建一個新的handler包裹next          handler := func(w http.ResponseWriter, r *http.Request) {                // 中間件的處理邏輯                          ......              // 調用下一個中間件或者最終的handler處理程序              next(w, r)          }            // 返回新建的包裝handler          return handler      }        // 返回新建的中間件      return middleware  }

使用中間件

我們創建兩個中間件,一個用於記錄程序執行的時長,另外一個用於驗證請求用的是否是指定的 HTTPMethod,創建完後再用定義的 Chain函數把 http.HandlerFunc和應用在其上的中間件鏈起來,中間件會按添加順序依次執行,最後執行到處理函數。完整的代碼如下:

package main    import (      "fmt"      "log"      "net/http"      "time"  )    type Middleware func(http.HandlerFunc) http.HandlerFunc    // 記錄每個URL請求的執行時長  func Logging() Middleware {        // 創建中間件      return func(f http.HandlerFunc) http.HandlerFunc {            // 創建一個新的handler包裝http.HandlerFunc          return func(w http.ResponseWriter, r *http.Request) {                // 中間件的處理邏輯              start := time.Now()              defer func() { log.Println(r.URL.Path, time.Since(start)) }()                // 調用下一個中間件或者最終的handler處理程序              f(w, r)          }      }  }    // 驗證請求用的是否是指定的HTTP Method,不是則返回 400 Bad Request  func Method(m string) Middleware {        return func(f http.HandlerFunc) http.HandlerFunc {            return func(w http.ResponseWriter, r *http.Request) {                if r.Method != m {                  http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)                  return              }                f(w, r)          }      }  }    // 把應用到http.HandlerFunc處理器的中間件  // 按照先後順序和處理器本身鏈起來供http.HandleFunc調用  func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {      for _, m := range middlewares {          f = m(f)      }      return f  }    // 最終的處理請求的http.HandlerFunc  func Hello(w http.ResponseWriter, r *http.Request) {      fmt.Fprintln(w, "hello world")  }    func main() {      http.HandleFunc("/", Chain(Hello, Method("GET"), Logging()))      http.ListenAndServe(":8080", nil)  }

運行程序後會打開瀏覽器訪問 http://localhost:8080會有如下輸出:

2020/02/07 21:07:52 / 359.503µs  2020/02/07 21:09:17 / 34.727µs

到這裡怎麼用 Go編寫和使用中間件就講完,也就十分鐘吧。不過這裡更多的是探究實現原理,那麼在生產環境怎麼自己使用編寫的這些中間件呢,我們接着往下看。

使用 gorilla/mux應用中間件

上面我們探討了如何創建中間件,但是使用上每次用 Chain函數鏈接多個中間件和處理程序還是有些不方便,而且在上一篇文章中我們已經開始使用 gorilla/mux提供的 Router作為路由器了。好在 gorrila.mux支持向路由器添加中間件,如果發現匹配項,則按照添加中間件的順序執行中間件,包括其子路由器也支持添加中間件。

gorrila.mux路由器使用 Use方法為路由器添加中間件, Use方法的定義如下:

func (r *Router) Use(mwf ...MiddlewareFunc) {      for _, fn := range mwf {          r.middlewares = append(r.middlewares, fn)      }  }

它可以接受多個 mux.MiddlewareFunc類型的參數, mux.MiddlewareFunc的類型聲明為:

type MiddlewareFunc func(http.Handler) http.Handler

跟我們上面定義的 Middleware類型很像也是一個函數類型,不過函數的參數和返回值都是 http.Handler接口,在《深入學習用 Go 編寫 HTTP 服務器》中我們詳細講過 http.Handler它 是 net/http中定義的接口用來表示處理 HTTP 請求的對象,其對象必須實現 ServeHTTP方法。我們把上面說的中間件模板稍微更改下就能創建符合 gorrila.mux要求的中間件:

func CreateMuxMiddleware() mux.MiddlewareFunc {        // 創建中間件      return func(f http.Handler) http.Handler {            // 創建一個新的handler包裝http.HandlerFunc          return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {                // 中間件的處理邏輯              ......                // 調用下一個中間件或者最終的handler處理程序              f.ServeHTTP(w, r)          })      }  }

接下來,我們把上面自定義的兩個中間件進行改造,然後應用到我們一直在使用的 http_demo項目上,為了便於管理在項目中新建 middleware目錄,兩個中間件分別放在 log.gohttp_method.go

//middleware/log.go  func Logging() mux.MiddlewareFunc {        // 創建中間件      return func(f http.Handler) http.Handler {            // 創建一個新的handler包裝http.HandlerFunc          return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {                // 中間件的處理邏輯              start := time.Now()              defer func() { log.Println(r.URL.Path, time.Since(start)) }()                // 調用下一個中間件或者最終的handler處理程序              f.ServeHTTP(w, r)          })      }  }    // middleware/http_demo.go  func Method(m string) mux.MiddlewareFunc {        return func(f http.Handler) http.Handler {            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {                if r.Method != m {                  http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)                  return              }                f.ServeHTTP(w, r)          })      }  }

然後在我們的路由器中進行引用:

func RegisterRoutes(r *mux.Router) {      r.Use(middleware.Logging())// 全局應用      indexRouter := r.PathPrefix("/index").Subrouter()      indexRouter.Handle("/", &handler.HelloHandler{})        userRouter := r.PathPrefix("/user").Subrouter()      userRouter.HandleFunc("/names/{name}/countries/{country}", handler.ShowVisitorInfo)      userRouter.Use(middleware.Method("GET"))//給子路由器應用  }

再次編譯啟動運行程序後訪問

http://localhost:8080/user/names/James/countries/NewZealand

從控制台里可以看到,記錄了這個請求的處理時長:

2020/02/08 09:29:50 Starting HTTP server...  2020/02/08 09:55:20 /user/names/James/countries/NewZealan 51.157µs

到這裡我們探究完了編寫Web中間件的過程和原理,在實際開發中只需要根據自己的需求按照我們給的中間件代碼模板編寫中間件即可,在編寫中間件的時候也要注意他們的職責範圍,不要所有邏輯都往裡放。