【深入理解 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)
    }
}

然而這樣還是有兩個問題:

目前的實現很可能造成以下後果:

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