Go 語(yǔ)言 中間件

2023-03-22 15:03 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-03-middleware.html


5.3 中間件

本章將對(duì)現(xiàn)在流行的 Web 框架中的中間件 (middleware) 技術(shù)原理進(jìn)行分析,并介紹如何使用中間件技術(shù)將業(yè)務(wù)和非業(yè)務(wù)代碼功能進(jìn)行解耦。

5.3.1 代碼泥潭

先來(lái)看一段代碼:

// 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)
    ...
}

這是一個(gè)典型的 Web 服務(wù),掛載了一個(gè)簡(jiǎn)單的路由。我們的線上服務(wù)一般也是從這樣簡(jiǎn)單的服務(wù)開始逐漸拓展開去的。

現(xiàn)在突然來(lái)了一個(gè)新的需求,我們想要統(tǒng)計(jì)之前寫的 hello 服務(wù)的處理耗時(shí),需求很簡(jiǎn)單,我們對(duì)上面的程序進(jìn)行少量修改:

// 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 請(qǐng)求時(shí),打印出當(dāng)前請(qǐng)求所消耗的時(shí)間。

完成了這個(gè)需求之后,我們繼續(xù)進(jìn)行業(yè)務(wù)開發(fā),提供的 API 逐漸增加,現(xiàn)在我們的路由看起來(lái)是這個(gè)樣子:

// 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)
    // ...
}

每一個(gè) handler 里都有之前提到的記錄運(yùn)行時(shí)間的代碼,每次增加新的路由我們也同樣需要把這些看起來(lái)長(zhǎng)得差不多的代碼拷貝到我們需要的地方去。因?yàn)榇a不太多,所以實(shí)施起來(lái)也沒(méi)有遇到什么大問(wèn)題。

漸漸的我們的系統(tǒng)增加到了 30 個(gè)路由和 handler 函數(shù),每次增加新的 handler ,我們的第一件工作就是把之前寫的所有和業(yè)務(wù)邏輯無(wú)關(guān)的周邊代碼先拷貝過(guò)來(lái)。

接下來(lái)系統(tǒng)安穩(wěn)地運(yùn)行了一段時(shí)間,突然有一天,老板找到你,我們最近找人新開發(fā)了監(jiān)控系統(tǒng),為了系統(tǒng)運(yùn)行可以更加可控,需要把每個(gè)接口運(yùn)行地耗時(shí)數(shù)據(jù)主動(dòng)上報(bào)到我們的系統(tǒng)監(jiān)控里。給監(jiān)控系統(tǒng)起個(gè)名字吧,叫 metrics?,F(xiàn)在你需要修改代碼并把耗時(shí)通過(guò) HTTP Post 的方式發(fā)給 metrics 系統(tǒng)了。我們來(lái)修改一下 ?helloHandler()? :

func helloHandler(wr http.ResponseWriter, r *http.Request) {
    timeStart := time.Now()
    wr.Write([]byte("hello"))
    timeElapsed := time.Since(timeStart)
    logger.Println(timeElapsed)
    // 新增耗時(shí)上報(bào)
    metrics.Upload("timeHandler", timeElapsed)
}

修改到這里,本能地發(fā)現(xiàn)我們的開發(fā)工作開始陷入了泥潭。無(wú)論未來(lái)對(duì)我們的這個(gè) Web 系統(tǒng)有任何其它的非功能或統(tǒng)計(jì)需求,我們的修改必然牽一發(fā)而動(dòng)全身。只要增加一個(gè)非常簡(jiǎn)單的非業(yè)務(wù)統(tǒng)計(jì),我們就需要去幾十個(gè) handler 里增加這些業(yè)務(wù)無(wú)關(guān)的代碼。雖然一開始我們似乎并沒(méi)有做錯(cuò),但是顯然隨著業(yè)務(wù)的發(fā)展,我們的行事方式讓我們陷入了代碼的泥潭。

5.3.2 使用中間件剝離非業(yè)務(wù)邏輯

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

我們犯的最大的錯(cuò)誤,是把業(yè)務(wù)代碼和非業(yè)務(wù)代碼揉在了一起。對(duì)于大多數(shù)的場(chǎng)景來(lái)講,非業(yè)務(wù)的需求都是在 http 請(qǐng)求處理前做一些事情,并且在響應(yīng)完成之后做一些事情。我們有沒(méi)有辦法使用一些重構(gòu)思路把這些公共的非業(yè)務(wù)功能代碼剝離出去呢?回到剛開頭的例子,我們需要給我們的 ?helloHandler()? 增加超時(shí)時(shí)間統(tǒng)計(jì),我們可以使用一種叫 ?function adapter? 的方法來(lái)對(duì) ?helloHandler()? 進(jìn)行包裝:


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)
    ...
}

這樣就非常輕松地實(shí)現(xiàn)了業(yè)務(wù)與非業(yè)務(wù)之間的剝離,魔法就在于這個(gè) timeMiddleware??梢詮拇a中看到,我們的 timeMiddleware() 也是一個(gè)函數(shù),其參數(shù)為 http.Handler,http.Handler 的定義在 net/http 包中:

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

任何方法實(shí)現(xiàn)了 ServeHTTP,即是一個(gè)合法的 http.Handler,讀到這里你可能會(huì)有一些混亂,我們先來(lái)梳理一下 http 庫(kù)的 Handler,HandlerFunc 和 ServeHTTP 的關(guān)系:

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

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

只要你的 handler 函數(shù)簽名是:

func (ResponseWriter, *Request)

那么這個(gè) handler 和 http.HandlerFunc() 就有了一致的函數(shù)簽名,可以將該 handler() 函數(shù)進(jìn)行類型轉(zhuǎn)換,轉(zhuǎn)為 http.HandlerFunc。而 http.HandlerFunc 實(shí)現(xiàn)了 http.Handler 這個(gè)接口。在 http 庫(kù)需要調(diào)用你的 handler 函數(shù)來(lái)處理 http 請(qǐng)求時(shí),會(huì)調(diào)用 HandlerFunc() 的 ServeHTTP() 函數(shù),可見(jiàn)一個(gè)請(qǐng)求的基本調(diào)用鏈?zhǔn)沁@樣的:

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

上面提到的把自定義 handler 轉(zhuǎn)換為 http.HandlerFunc() 這個(gè)過(guò)程是必須的,因?yàn)槲覀兊?nbsp;handler 沒(méi)有直接實(shí)現(xiàn) ServeHTTP 這個(gè)接口。上面的代碼中我們看到的 HandleFunc(注意 HandlerFunc 和 HandleFunc 的區(qū)別) 里也可以看到這個(gè)強(qiáng)制轉(zhuǎn)換過(guò)程:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

// 調(diào)用

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    mux.Handle(pattern, HandlerFunc(handler))
}

知道 handler 是怎么一回事,我們的中間件通過(guò)包裝 handler,再返回一個(gè)新的 handler 就好理解了。

總結(jié)一下,我們的中間件要做的事情就是通過(guò)一個(gè)或多個(gè)函數(shù)對(duì) handler 進(jìn)行包裝,返回一個(gè)包括了各個(gè)中間件邏輯的函數(shù)鏈。我們把上面的包裝再做得復(fù)雜一些:

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

這個(gè)函數(shù)鏈在執(zhí)行過(guò)程中的上下文可以用 圖 5-8 來(lái)表示。


圖 5-8 請(qǐng)求處理過(guò)程

再直白一些,這個(gè)流程在進(jìn)行請(qǐng)求處理的時(shí)候就是不斷地進(jìn)行函數(shù)壓棧再出棧,有一些類似于遞歸的執(zhí)行流:

[exec of logger logic]		   函數(shù)棧: []

[exec of timeout logic]		  函數(shù)棧: [logger]

[exec of ratelimit logic]		函數(shù)棧: [timeout/logger]

[exec of helloHandler logic]	 函數(shù)棧: [ratelimit/timeout/logger]

[exec of ratelimit logic part2]  函數(shù)棧: [timeout/logger]

[exec of timeout logic part2]	函數(shù)棧: [logger]

[exec of logger logic part2]	 函數(shù)棧: []

功能實(shí)現(xiàn)了,但在上面的使用過(guò)程中我們也看到了,這種函數(shù)套函數(shù)的用法不是很美觀,同時(shí)也不具備什么可讀性。

5.3.3 更優(yōu)雅的中間件寫法

上一節(jié)中解決了業(yè)務(wù)功能代碼和非業(yè)務(wù)功能代碼的解耦,但也提到了,看起來(lái)并不美觀,如果需要修改這些函數(shù)的順序,或者增刪中間件還是有點(diǎn)費(fèi)勁,本節(jié)我們來(lái)進(jìn)行一些”寫法“上的優(yōu)化。

看一個(gè)例子:

r = NewRouter()
r.Use(logger)
r.Use(timeout)
r.Use(ratelimit)
r.Add("/", helloHandler)

通過(guò)多步設(shè)置,我們擁有了和上一節(jié)差不多的執(zhí)行函數(shù)鏈。勝在直觀易懂,如果我們要增加或者刪除中間件,只要簡(jiǎn)單地增加刪除對(duì)應(yīng)的 Use() 調(diào)用就可以了。非常方便。

從框架的角度來(lái)講,怎么實(shí)現(xiàn)這樣的功能呢?也不復(fù)雜:

type middleware func(http.Handler) http.Handler

type Router struct {
    middlewareChain [] middleware
    mux map[string] http.Handler
}

func NewRouter() *Router {
    return &Router{
        mux: make(map[string]http.Handler),
    }
}

func (r *Router) Use(m middleware) {
    r.middlewareChain = append(r.middlewareChain, m)
}

func (r *Router) Add(route string, h http.Handler) {
    var mergedHandler = h

    for i := len(r.middlewareChain) - 1; i >= 0; i-- {
        mergedHandler = r.middlewareChain[i](mergedHandler)
    }

    r.mux[route] = mergedHandler
}

注意代碼中的 middleware 數(shù)組遍歷順序,和用戶希望的調(diào)用順序應(yīng)該是 "相反" 的。應(yīng)該不難理解。

5.3.4 哪些事情適合在中間件中做

以較流行的開源 Go 語(yǔ)言框架 chi 為例:

compress.go
  => 對(duì) http 的響應(yīng)體進(jìn)行壓縮處理
heartbeat.go
  => 設(shè)置一個(gè)特殊的路由,例如 / ping,/healthcheck,用來(lái)給負(fù)載均衡一類的前置服務(wù)進(jìn)行探活
logger.go
  => 打印請(qǐng)求處理處理日志,例如請(qǐng)求處理時(shí)間,請(qǐng)求路由
profiler.go
  => 掛載 pprof 需要的路由,如 `/pprof`、`/pprof/trace` 到系統(tǒng)中
realip.go
  => 從請(qǐng)求頭中讀取 X-Forwarded-For 和 X-Real-IP,將 http.Request 中的 RemoteAddr 修改為得到的 RealIP
requestid.go
  => 為本次請(qǐng)求生成單獨(dú)的 requestid,可一路透?jìng)?,用?lái)生成分布式調(diào)用鏈路,也可用于在日志中串連單次請(qǐng)求的所有邏輯
timeout.go
  => 用 context.Timeout 設(shè)置超時(shí)時(shí)間,并將其通過(guò) http.Request 一路透?jìng)飨氯?throttler.go
  => 通過(guò)定長(zhǎng)大小的 channel 存儲(chǔ) token,并通過(guò)這些 token 對(duì)接口進(jìn)行限流

每一個(gè) Web 框架都會(huì)有對(duì)應(yīng)的中間件組件,如果你有興趣,也可以向這些項(xiàng)目貢獻(xiàn)有用的中間件,只要合理一般項(xiàng)目的維護(hù)人也愿意合并你的 Pull Request。

比如開源界很火的 gin 這個(gè)框架,就專門為用戶貢獻(xiàn)的中間件開了一個(gè)倉(cāng)庫(kù),見(jiàn)圖 5-9


圖 5-9 gin 的中間件倉(cāng)庫(kù)

如果讀者去閱讀 gin 的源碼的話,可能會(huì)發(fā)現(xiàn) gin 的中間件中處理的并不是 http.Handler,而是一個(gè)叫 gin.HandlerFunc 的函數(shù)類型,和本節(jié)中講解的 http.Handler 簽名并不一樣。不過(guò) gin 的 handler 也只是針對(duì)其框架的一種封裝,中間件的原理與本節(jié)中的說(shuō)明是一致的。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)