关于golang:深入理解Go从0到1实现一个filtermiddleware

12次阅读

共计 5227 个字符,预计需要花费 14 分钟才能阅读完成。

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 治理框架

正文完
 0