原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-03-middleware.html
本章將對現(xiàn)在流行的 Web 框架中的中間件 (middleware) 技術(shù)原理進行分析,并介紹如何使用中間件技術(shù)將業(yè)務(wù)和非業(yè)務(wù)代碼功能進行解耦。
先來看一段代碼:
// 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 服務(wù),掛載了一個簡單的路由。我們的線上服務(wù)一般也是從這樣簡單的服務(wù)開始逐漸拓展開去的。
現(xiàn)在突然來了一個新的需求,我們想要統(tǒng)計之前寫的 hello 服務(wù)的處理耗時,需求很簡單,我們對上面的程序進行少量修改:
// 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 請求時,打印出當(dāng)前請求所消耗的時間。
完成了這個需求之后,我們繼續(xù)進行業(yè)務(wù)開發(fā),提供的 API 逐漸增加,現(xiàn)在我們的路由看起來是這個樣子:
// 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 里都有之前提到的記錄運行時間的代碼,每次增加新的路由我們也同樣需要把這些看起來長得差不多的代碼拷貝到我們需要的地方去。因為代碼不太多,所以實施起來也沒有遇到什么大問題。
漸漸的我們的系統(tǒng)增加到了 30 個路由和 handler 函數(shù),每次增加新的 handler ,我們的第一件工作就是把之前寫的所有和業(yè)務(wù)邏輯無關(guān)的周邊代碼先拷貝過來。
接下來系統(tǒng)安穩(wěn)地運行了一段時間,突然有一天,老板找到你,我們最近找人新開發(fā)了監(jiān)控系統(tǒng),為了系統(tǒng)運行可以更加可控,需要把每個接口運行地耗時數(shù)據(jù)主動上報到我們的系統(tǒng)監(jiān)控里。給監(jiān)控系統(tǒng)起個名字吧,叫 metrics。現(xiàn)在你需要修改代碼并把耗時通過 HTTP Post 的方式發(fā)給 metrics 系統(tǒng)了。我們來修改一下 ?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)
}
修改到這里,本能地發(fā)現(xiàn)我們的開發(fā)工作開始陷入了泥潭。無論未來對我們的這個 Web 系統(tǒng)有任何其它的非功能或統(tǒng)計需求,我們的修改必然牽一發(fā)而動全身。只要增加一個非常簡單的非業(yè)務(wù)統(tǒng)計,我們就需要去幾十個 handler 里增加這些業(yè)務(wù)無關(guān)的代碼。雖然一開始我們似乎并沒有做錯,但是顯然隨著業(yè)務(wù)的發(fā)展,我們的行事方式讓我們陷入了代碼的泥潭。
我們來分析一下,一開始在哪里做錯了呢?我們只是一步一步地滿足需求,把我們需要的邏輯按照流程寫下去呀?
我們犯的最大的錯誤,是把業(yè)務(wù)代碼和非業(yè)務(wù)代碼揉在了一起。對于大多數(shù)的場景來講,非業(yè)務(wù)的需求都是在 http 請求處理前做一些事情,并且在響應(yīng)完成之后做一些事情。我們有沒有辦法使用一些重構(gòu)思路把這些公共的非業(yè)務(wù)功能代碼剝離出去呢?回到剛開頭的例子,我們需要給我們的 ?helloHandler()
? 增加超時時間統(tǒng)計,我們可以使用一種叫 ?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)
...
}
這樣就非常輕松地實現(xiàn)了業(yè)務(wù)與非業(yè)務(wù)之間的剝離,魔法就在于這個 timeMiddleware
??梢詮拇a中看到,我們的 timeMiddleware()
也是一個函數(shù),其參數(shù)為 http.Handler
,http.Handler
的定義在 net/http
包中:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
任何方法實現(xiàn)了 ServeHTTP
,即是一個合法的 http.Handler
,讀到這里你可能會有一些混亂,我們先來梳理一下 http 庫的 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)
那么這個 handler 和 http.HandlerFunc()
就有了一致的函數(shù)簽名,可以將該 handler()
函數(shù)進行類型轉(zhuǎn)換,轉(zhuǎn)為 http.HandlerFunc
。而 http.HandlerFunc
實現(xiàn)了 http.Handler
這個接口。在 http
庫需要調(diào)用你的
handler 函數(shù)來處理 http 請求時,會調(diào)用 HandlerFunc()
的 ServeHTTP()
函數(shù),可見一個請求的基本調(diào)用鏈是這樣的:
h = getHandler() => h.ServeHTTP(w, r) => h(w, r)
上面提到的把自定義 handler
轉(zhuǎn)換為 http.HandlerFunc()
這個過程是必須的,因為我們的 handler
沒有直接實現(xiàn) ServeHTTP
這個接口。上面的代碼中我們看到的 HandleFunc(注意 HandlerFunc 和 HandleFunc 的區(qū)別) 里也可以看到這個強制轉(zhuǎn)換過程:
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 是怎么一回事,我們的中間件通過包裝 handler,再返回一個新的 handler 就好理解了。
總結(jié)一下,我們的中間件要做的事情就是通過一個或多個函數(shù)對 handler 進行包裝,返回一個包括了各個中間件邏輯的函數(shù)鏈。我們把上面的包裝再做得復(fù)雜一些:
customizedHandler = logger(timeout(ratelimit(helloHandler)))
這個函數(shù)鏈在執(zhí)行過程中的上下文可以用 圖 5-8 來表示。
圖 5-8 請求處理過程
再直白一些,這個流程在進行請求處理的時候就是不斷地進行函數(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ù)棧: []
功能實現(xiàn)了,但在上面的使用過程中我們也看到了,這種函數(shù)套函數(shù)的用法不是很美觀,同時也不具備什么可讀性。
上一節(jié)中解決了業(yè)務(wù)功能代碼和非業(yè)務(wù)功能代碼的解耦,但也提到了,看起來并不美觀,如果需要修改這些函數(shù)的順序,或者增刪中間件還是有點費勁,本節(jié)我們來進行一些”寫法“上的優(yōu)化。
看一個例子:
r = NewRouter()
r.Use(logger)
r.Use(timeout)
r.Use(ratelimit)
r.Add("/", helloHandler)
通過多步設(shè)置,我們擁有了和上一節(jié)差不多的執(zhí)行函數(shù)鏈。勝在直觀易懂,如果我們要增加或者刪除中間件,只要簡單地增加刪除對應(yīng)的 Use()
調(diào)用就可以了。非常方便。
從框架的角度來講,怎么實現(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)該不難理解。
以較流行的開源 Go 語言框架 chi 為例:
compress.go
=> 對 http 的響應(yīng)體進行壓縮處理
heartbeat.go
=> 設(shè)置一個特殊的路由,例如 / ping,/healthcheck,用來給負載均衡一類的前置服務(wù)進行探活
logger.go
=> 打印請求處理處理日志,例如請求處理時間,請求路由
profiler.go
=> 掛載 pprof 需要的路由,如 `/pprof`、`/pprof/trace` 到系統(tǒng)中
realip.go
=> 從請求頭中讀取 X-Forwarded-For 和 X-Real-IP,將 http.Request 中的 RemoteAddr 修改為得到的 RealIP
requestid.go
=> 為本次請求生成單獨的 requestid,可一路透傳,用來生成分布式調(diào)用鏈路,也可用于在日志中串連單次請求的所有邏輯
timeout.go
=> 用 context.Timeout 設(shè)置超時時間,并將其通過 http.Request 一路透傳下去
throttler.go
=> 通過定長大小的 channel 存儲 token,并通過這些 token 對接口進行限流
每一個 Web 框架都會有對應(yīng)的中間件組件,如果你有興趣,也可以向這些項目貢獻有用的中間件,只要合理一般項目的維護人也愿意合并你的 Pull Request。
比如開源界很火的 gin 這個框架,就專門為用戶貢獻的中間件開了一個倉庫,見圖 5-9:
圖 5-9 gin 的中間件倉庫
如果讀者去閱讀 gin 的源碼的話,可能會發(fā)現(xiàn) gin 的中間件中處理的并不是 http.Handler
,而是一個叫 gin.HandlerFunc
的函數(shù)類型,和本節(jié)中講解的 http.Handler
簽名并不一樣。不過 gin 的 handler
也只是針對其框架的一種封裝,中間件的原理與本節(jié)中的說明是一致的。
![]() | ![]() |
更多建議: