十分鐘學會用 Go 編寫 Web 中介軟體

KevinYan發表於2020-02-08

本文首發於公眾號,關注文末公眾號回覆gohttp03 獲取文章所用完整原始碼。

中介軟體(通常)是一小段程式碼,它們接受一個請求,對其進行處理,每個中介軟體只處理一件事情,完成後將其傳遞給另一箇中介軟體或最終處理程式,這樣就做到了程式的解耦。如果沒有中介軟體那麼我們必須在最終的處理程式中來完成這些處理操作,這無疑會造成處理程式的臃腫和程式碼複用率不高的問題。中介軟體的一些常見用例是請求日誌記錄,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
}

使用中介軟體

我們建立兩個中介軟體,一個用於記錄程式執行的時長,另外一個用於驗證請求用的是否是指定的HTTP Method,建立完後再用定義的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中介軟體的過程和原理,在實際開發中只需要根據自己的需求按照我們給的中介軟體程式碼模板編寫中介軟體即可,在編寫中介軟體的時候也要注意他們的職責範圍,不要所有邏輯都往裡放。

前文回顧:

深入學習用 Go 編寫 HTTP 伺服器

使用gorilla/mux增強Go HTTP伺服器的路由能力

在公眾號裡關鍵字回覆gohttp03可以拿到本篇文章中完整的原始碼,喜歡我的文章幫忙轉發點贊。

tWbHIMFsM3.png

本作品採用《CC 協議》,轉載必須註明作者和本文連結

公眾號:網管叨bi叨 | Golang、PHP、Laravel、Docker等學習經驗分享

相關文章