原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-03-middleware.html
本章將對(duì)現(xiàn)在流行的 Web 框架中的中間件 (middleware) 技術(shù)原理進(jìn)行分析,并介紹如何使用中間件技術(shù)將業(yè)務(wù)和非業(yè)務(wù)代碼功能進(jìn)行解耦。
先來(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ā)展,我們的行事方式讓我們陷入了代碼的泥潭。
我們來(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í)也不具備什么可讀性。
上一節(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)該不難理解。
以較流行的開源 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ō)明是一致的。
![]() | ![]() |
更多建議: