【深入理解 Go】從 0 到 1 實現一個 filter
filter(也稱 middleware)是我們平時業務中用的非常廣泛的框架組件,很多 web 框架、微服務框架都有集成。通常在一些請求的前後,我們會把比較通用的邏輯都會放到 filter 組件來實現。如打請求日誌、耗時、權限、接口限流等通用邏輯。那麼接下來我會和你一起實現一個 filter 組件,同時讓你瞭解到,它是如何從 0 到 1 搭建起來的,具體在演進過程中遇到了哪些問題,是如何解決的。
從一個簡單的 server 說起
我們看這樣一段代碼。首先我們在服務端開啓了一個 http server,配置了 / 這個路由,hello 函數處理這個路由的請求,並往 body 中寫入 hello 字符串響應給客戶端。我們通過訪問 127.0.0.1:8080 就可以看到響應結果。具體的實現如下:
// 模擬業務代碼
func hello(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello"))
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
打印請求耗時 v1.0
接下來有一個需求,需要打印這個請求執行的時間,這個也是我們業務中比較常見的場景。我們可能會這樣實現,在 hello 這個 handler 方法中加入時間計算邏輯,主函數不變:
// 模擬業務代碼
func hello(wr http.ResponseWriter, r *http.Request) {
// 增加計算執行時間邏輯
start := time.Now()
wr.Write([]byte("hello"))
timeElapsed := time.Since(start)
// 打印請求耗時
fmt.Println(timeElapsed)
}
func main() {
http.HandleFunc("/", hello)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
但是這樣實現仍然有一定問題。假設我們有一萬個請求路徑定義、所以有一萬個 handler 和它對應,我們在這一萬個 handler 中,如果都要加上請求執行時間的計算,那必然代價是相當大的。
爲了提升代碼複用率,我們使用 filter 組件來解決此類問題。大多數 web 框架或微服務框架都提供了這個組件,在有些框架中也叫做 middleware。
filter 登場
filter 的基本思路,是把功能性(業務代碼)與非功能性(非業務代碼)分離,保證對業務代碼無侵入,同時提高代碼複用性。在講解 2.0 的需求實現之前,我們先回顧一下 1.0 中比較重要的函數調用 http.HandleFunc("/", hello)
這個函數會接收一個路由規則 pattern,以及這個路由對應的處理函數 handler。我們一般的業務邏輯都會寫在 handler 裏面,在這裏就是 hello 函數。我們接下來看一下 http.HandleFunc() 函數的詳細定義:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
這裏要注意一下,標準庫中又把 func(ResponseWriter, *Request) 這個 func 重新定義成一個類型別名 HandlerFunc:
type HandlerFunc func(ResponseWriter, *Request)
所以我們一開始用的 http.HandleFunc() 函數定義,可以直接簡化成這樣:
func HandleFunc(pattern string, handler HandlerFunc)
我們只要把「HandlerFunc 類型」與「HandleFunc 函數」區分開就可以一目瞭然了。因爲 hello 這個用戶函數也符合 HandlerFunc 這個類型的定義,所以自然可以直接傳給 http.HandlerFunc 函數。而 HandlerFunc 類型其實是 Handler 接口的一個實現,Handler 接口的實現如下,它只有 ServeHTTP 這一個方法:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
HandlerFunc 就是標準庫中提供的默認的 Handler 接口實現,所以它要實現 ServeHTTP 方法。它在 ServeHTTP 中只做了一件事,那就是調用用戶傳入的 handler,執行具體的業務邏輯,在我們這裏就是執行 hello(),打印字符串,整個請求響應流程結束
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
打印請求耗時 v2.0
所以我們能想到的比較容易的辦法,就是把傳入的用戶業務函數 hello 在外面包一層,而非在 hello 裏面去加打印時間的代碼。我們可以單獨定義一個 timeFilter 函數,他接收一個參數 f,也是 http.HandlerFunc 類型,然後在我們傳入的 f 前後加上的 time.Now、time.Since 代碼。
這裏注意,timeFilter 最終返回值也是一個 http.HandlerFunc 函數類型,因爲畢竟最終還是要傳給 http.HandleFunc 函數的,所以 filter 必須也要返回這個類型,這樣就可以實現最終業務代碼與非業務代碼分離的同時,實現打印請求時間。詳細實現如下:
// 打印請求時間filter,和具體的業務邏輯hello解耦
func timeFilter(f http.HandlerFunc) http.HandlerFunc {
return func(wr http.ResponseWriter, r *http.Request) {
start := time.Now()
// 這裏就是上面我們看過HandlerFun類型中ServeHTTP的默認實現,會直接調用f()執行業務邏輯,這裏就是我們的hello,最終會打印出字符串
f.ServeHTTP(wr, r)
timeElapsed := time.Since(start)
// 打印請求耗時
fmt.Println(timeElapsed)
}
}
func hello(wr http.ResponseWriter, r *http.Request) {
wr.Write([]byte("hello\n"))
}
func main() {
// 在hello的外面包上一層timeFilter
http.HandleFunc("/", timeFilter(hello))
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
然而這樣還是有兩個問題:
-
如果有十萬個路由,那我要在這十萬個路由上,每個都去加上相同的包裹代碼嗎?
-
如果有十萬個 filter,那我們要包裹十萬層嗎,代碼可讀性會非常差
目前的實現很可能造成以下後果:
http.HandleFunc("/123", filter3(filter2(filter1(hello))))
http.HandleFunc("/456", filter3(filter2(filter1(hello))))
http.HandleFunc("/789", filter3(filter2(filter1(hello))))
http.HandleFunc("/135", filter3(filter2(filter1(hello))))
...
那麼如何更優雅的去管理 filter 與路由之間的關係,能夠讓 filter3(filter2(filter1(hello))) 只寫一次就能作用到所有路由上呢?
打印請求耗時 v3.0
我們可以想到,我們先把 filter 的定義抽出來單獨定義爲 Filter 類型,然後可以定義一個結構體 Frame,裏面的 filters 字段用來專門管理所有的 filter。這裏可以從 main 函數看起。我們添加了 timeFilter、路由、最終開啓服務,大體上和 1.0 版本的流程是一樣的:
// Filter類型定義
type Filter func(f http.HandlerFunc) http.HandlerFunc
type Frame struct {
// 存儲所有註冊的過濾器
filters []Filter
}
// AddFilter 註冊filter
func (r *Frame) AddFilter(filter Filter) {
r.filters = append(r.filters, filter)
}
// AddRoute 註冊路由,並把handler按filter添加順序包起來。這裏採用了遞歸實現比較好理解,後面會講迭代實現
func (r *Frame) AddRoute(pattern string, f http.HandlerFunc) {
r.process(pattern, f, len(r.filters) - 1)
}
func (r *Frame) process(pattern string, f http.HandlerFunc, index int) {
if index == -1 {
http.HandleFunc(pattern, f)
return
}
fWrap := r.filters[index](f)
index--
r.process(pattern, fWrap, index)
}
// Start 框架啓動
func (r *Frame) Start() {
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
func main() {
r := &Frame{}
r.AddFilter(timeFilter)
r.AddFilter(logFilter)
r.AddRoute("/", hello)
r.Start()
}
r.AddRoute 之前都很好理解,初始化主結構,並把我們定義好的 filter 放到主結構中的切片統一管理。接下來 AddRoute 這裏是核心邏輯,接下來我們詳細講解一下
AddRoute
r.AddRoute("/", hello) 其實和 v1.0 裏的 http.HandleFunc("/", hello) 其實一摸一樣,只不過內部增加了 filter 的邏輯。在 r.AddRoute 內部會調用 process 函數,我將參數全部替換成具體的值:
r.process("/", hello, 1)
那麼在 process 內部,首先 index 不等於 - 1,往下執行到
fWrap := r.filters[index](f)
他的含義就是,取出第 index 個 filter,當前是 r.filters[1],r.filters[1] 就是我們的 logFilter,logFilter 接收一個 f(這裏就是 hello),logFilter 裏的 f.ServerHTTP 可以直接看成執行 f(),即 hello,相當於直接用 hello 裏的邏輯替換掉了 logFilter 裏的 f.ServerHTTP 這一行,在下圖裏用箭頭表示。最後將 logFilter 的返回值賦值給 fWrap,將包裹後的 fWrap 繼續往下遞歸,index--:
同理,接下來的遞歸參數爲:
r.process("/", hello, 0)
這裏就輪到 r.filters[0] 了,即 timeFilter,過程同上:
最後一輪遞歸,index = -1,即所有 filter 都處理完了,我們就可以最終和 v1.0 一樣,調用 http.HandleFunc(pattern, f) 將最終我們層層包裹後的 f,最終註冊上去,整個流程結束:
AddRoute 的遞歸版本相對容易理解,我也同樣用迭代實現了一個版本。每次循環會在本層 filter 將 f 包裹後重新賦值給 f,這樣就可以將之前包裹後的 f 沿用到下一輪迭代,基於上一輪的 f 繼續包裹剩餘的 filter。在 gin 框架中就用了迭代這種方式來實現:
// AddRouteIter AddRoute的迭代實現
func (r *Frame) AddRouteIter(pattern string, f http.HandlerFunc) {
filtersLen := len(r.filters)
for i := filtersLen; i >= 0; i-- {
f = r.filters[i](f)
}
http.HandleFunc(pattern, f)
}
這種 filter 的實現也叫做洋蔥模式,最裏層是我們的業務邏輯 helllo,然後外面是 logFilter、在外面是 timeFilter,很像這個洋蔥,相信到這裏你已經可以體會到了:
小結
我們從最開始 1.0 版本業務邏輯和非業務邏輯耦合嚴重,到 2.0 版本引入 filter 但實現仍不優雅,到 3.0 版本解決 2.0 版本的遺留問題,最終實現了一個簡易的 filter 管理框架
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/0VvfU9JkZqLYH2Z4iHBZdg