Go語言middleware:Web中介軟體

2020-07-16 10:05:01
本節將對現在流行的 Web 框架中的中介軟體 (middleware) 技術原理進行分析,並介紹如何使用中介軟體技術將業務和非業務程式碼功能進行解耦。

為什麼使用中介軟體

先來看一段程式碼:
// middleware/hello.go
package main
func hello(wr http.ResponseWriter, r *http.Request) {
    wr.Write([]byte("hello"))
}
func main() {
    http.HandleFunc("/", hello)
    err := http.ListenAndServe(":8080", nil)
    ...
}
這是一個典型的 Web 服務,掛載了一個簡單的路由。我們的線上服務一般也是從這樣簡單的服務開始逐漸拓展開去的。

現在突然來了一個新的需求,我們想要統計之前寫的 hello 服務的處理耗時,需求很簡單,我們對上面的程式進行少量修改:
// middleware/hello_with_time_elapse.go
var logger = log.New(os.Stdout, "", 0)
func hello(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
}
這樣便可以在每次接收到 http 請求時,列印出當前請求所消耗的時間。

完成了這個需求之後,我們繼續進行業務開發,提供的 API 逐漸增加,現在我們的路由看起來是這個樣子:
// middleware/hello_with_more_routes.go
// 省略了一些相同的程式碼
package main
func helloHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
}
func showInfoHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
}
func showEmailHandler(wr http.ResponseWriter, r *http.Request) {
    // ...
}
func showFriendsHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("your friends is tom and alex"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
}
func main() {
    http.HandleFunc("/", helloHandler)
    http.HandleFunc("/info/show", showInfoHandler)
    http.HandleFunc("/email/show", showEmailHandler)
    http.HandleFunc("/friends/show", showFriendsHandler)
    // ...
}
每一個 handler 裡都有之前提到的記錄執行時間的程式碼,每次增加新的路由我們也同樣需要把這些看起來長得差不多的程式碼拷貝到我們需要的地方去。因為程式碼不太多,所以實施起來也沒有遇到什麼大問題。

漸漸的我們的系統增加到了 30 個路由和 handler 函數,每次增加新的 handler,我們的第一件工作就是把之前寫的所有和業務邏輯無關的周邊程式碼先拷貝過來。

接下來系統安穩地執行了一段時間,突然有一天,老闆找到你,我們最近找人新開發了監控系統,為了系統執行可以更加可控,需要把每個介面執行的耗時資料主動上報到我們的監控系統裡。給監控系統起個名字吧,叫 metrics。現在需要修改程式碼並把耗時通過 HTTP Post 的方式發給 metrics 系統了。我們來修改一下 helloHandler():
func helloHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
    // 新增耗時上報
    metrics.Upload("timeHandler", timeElapsed)
}
修改到這裡,本能地發現我們的開發工作開始陷入了泥潭。無論未來對我們的這個 Web 系統有任何其它的非功能或統計需求,我們的修改必然牽一髮而動全身。只要增加一個非常簡單的非業務統計,我們就需要去幾十個 handler 裡增加這些業務無關的程式碼。雖然一開始我們似乎並沒有做錯,但是顯然隨著業務的發展,我們的行事方式讓我們陷入了程式碼的泥潭。

使用中介軟體剝離非業務邏輯

我們來分析一下,一開始在哪裡做錯了呢?我們只是一步一步地滿足需求,把我們需要的邏輯按照流程寫下去呀?

實際上,我們犯的最大的錯誤是把業務程式碼和非業務程式碼揉在了一起。對於大多數的場景來講,非業務的需求都是在 http 請求處理前做一些事情,並且在響應完成之後做一些事情。我們有沒有辦法使用一些重構思路把這些公共的非業務功能程式碼剝離出去呢?

回到剛開頭的例子,我們需要給我們的 helloHandler() 增加超時時間統計,我們可以使用一種叫 function adapter 的方法來對 helloHandler() 進行包裝:
func hello(wr http.ResponseWriter, r *http.Request) {
    wr.Write([]byte("hello"))
}
func timeMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(wr http.ResponseWriter, r *http.Request) {
        timeStart := time.Now()
        // next handler
        next.ServeHTTP(wr, r)
        timeElapsed := time.Since(timeStart)
        logger.Println(timeElapsed)
    })
}
func main() {
    http.Handle("/", timeMiddleware(http.HandlerFunc(hello)))
    err := http.ListenAndServe(":8080", nil)
    ...
}
這樣就非常輕鬆地實現了業務與非業務之間的剝離,魔法就在於這個 timeMiddleware 。可以從程式碼中看到,我們的 timeMiddleware() 也是一個函數,其引數為 http.Handler,http.Handler 的定義在 net/http 包中:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

任何方法實現了 ServeHTTP ,即是一個合法的 http.Handler,讀到這裡大家可能會有一些混亂,我們先來梳理一下 http 庫的 Handler,HandlerFunc 和 ServeHTTP 的關係:
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
實際上只要你的 handler 函數簽名是:

func (ResponseWriter, *Request)

那麼這個 handler 和 http.HandlerFunc() 就有了一致的函數簽名,可以將該 handler() 函數進行型別轉換,轉為 http.HandlerFunc。

而 http.HandlerFunc 實現了 http.Handler 這個介面。在 http 庫需要呼叫的 handler 函數來處理 http 請求時,會呼叫 HandlerFunc() 的 ServeHTTP() 函數,可見一個請求的基本呼叫鏈是這樣的:

h = getHandler() => h.ServeHTTP(w, r) => h(w, r)

上面提到的把自定義 handler 轉換為 http.HandlerFunc() 這個過程是必須的,因為我們的 handler 沒有直接實現 ServeHTTP 這個介面。上面的程式碼中我們看到的 HandleFunc( 注意 HandlerFunc 和 HandleFunc 的區別 ) 裡也可以看到這個強制轉換過程:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}
// 呼叫
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}
知道 handler 是怎麼一回事,我們的中介軟體通過包裝 handler,再返回一個新的 handler 就好理解了。

總結一下,我們的中介軟體要做的事情就是通過一個或多個函數對 handler 進行包裝,返回一個包括了各個中介軟體邏輯的函數鏈。我們把上面的包裝再做得複雜一些:

customizedHandler = logger(timeout(ratelimit(helloHandler)))

這個函數鏈在執行過程中的上下文可以用下圖來表示。

請求處理過程
圖:請求處理過程