今天来讨论一下流行、常见的具有 RESTful API 风格的 web 框架中的原理,涵盖了 gin、go-chi 和 ego 的一些组件。我们会发现,它们框架中的组件,或多或少会有以下几个设计:middleware、router、validator、request binder、sql binder,。或许名称不都叫这个,但其实现的逻辑大概是差不多的。下面就来具体讲解它们的实现。
Middleware
实际场景
在讲 middleware 底层实现之前,我们先来看看它在我们实际项目中是怎么用到的:
1package main
2
3func hello(wr http.ResponseWriter, r *http.Request) {
4 wr.Write([]byte("hello"))
5}
6
7func main() {
8 http.HandleFunc("/", hello)
9 err := http.ListenAndServe(":8080", nil)
10 ......
11}
这里我们有个 hello world 的业务逻辑,监听了 8080 端口,配置了一个 http 的 Handler,只要访问了路由 / 就会进入 hello 的逻辑。内部的逻辑这里只有个简单的写动作,实际场景中还会不断地叠加业务逻辑。
某一天新的业务来了,想要统计每个请求所花费的时间,怎么实现呢?
1// mindware/hello_with_time.elapse.go
2var logger = log.New(os.Stdout, "", 0)
3
4func hello(we http.ReponseWriter, r *http.Request) {
5 timeStart := time.Now()
6 we.Write([]byte("hello"))
7 timeElapsed := time.Since(timeStart)
8 logger.Println(timeElapsed)
9}
最简单粗暴的就是在 hello 逻辑中手动添加统计时间的过程,在向 http.ResponseWriter 写入 hello 逻辑之前记录当前的时间,在处理完业务逻辑以后,再记录消耗过的时间,并且把中间消耗过的时间打到日志中。
随着业务的迭代,接口肯定会逐渐增加。我们一个模块不可能只有一个诸如 hello 这样简单的接口,还会有各种各样的接口。虽然大多数公司都是微服务架构,但一般一个模块中至少有 10 个以上的接口,这种“笨笨”的办法就不是很适用了。哪怕现在年轻体力好,有 100 个接口,也可以写这样的代码一百遍,随着公司发展壮大后,除了要把请求的耗时写到日志里之外,可能业务还需要将耗时上传到可视化监控当中,代码就会发生如下改动:
1func hello(we http.ReponseWriter, r *http.Request) {
2 timeStart := time.Now()
3 we.Write([]byte("hello"))
4 timeElapsed := time.Since(timeStart)
5 logger.Println(timeElapsed)
6 // 新增耗时上报功能
7 metrics.Upload("timeHandler", timeElapsed)
8}
洋葱模式
我们有 100 个接口都有这种功能修改需求,而如果有几百上千个接口,每个接口都要改一遍?所以我们有了中间件的思想,有个框架中叫做 middleware 有点框架中叫 filter (Java 框架中多这么叫,比如 Spring),中间件的本质其实就是实现了 23 种设计模式中的一种,责任链模式 (Chain of Responsibility Pattern),或者叫做拦截器模式,又或者叫装饰器模式,还有的地方称之为代理模式、洋葱模式等等,它们的实现都是差不多的东西。
实现责任链模式的基本思路是,把业务代码中功能性和非业务代码中非功能性的代码分离。具体实现如下:
1func hello(wr http.ResponseWriter, r *http.Request) {
2 wr.Write([]byte("hello"))
3}
4
5func timeMiddleware(next http.Handler) http.Handler {
6 return http.HandlerFunc(func(we http.ResponseWriter, r *http.Request){
7 timeStart := time.Now()
8
9 // next handler
10 next.ServeHTTP(wr, r)
11
12 timeElapsed := time.Since(timeStart)
13 logger.Println(timeElapsed)
14 })
15}
16
17func main() {
18 http.HandleFunc("/", timeMiddleware(http.HandlerFunc(hello)))
19 err := http.ListenAndServe(":8080", nil)
20 ......
21}
我们要实现统计耗时的中间件,声明一个叫做 timeMiddleware 的函数,它接受一个 http.Handler 的参数,并且返回同类型的参数。其中,传入参数 next 就是实际业务的 Handler,那么我们在返回这个 Handler 之前,先计一个时,在 next.ServeHTTP(wr, r) 执行用户传进来的整套业务逻辑流程,执行完成之后再去记录新的时间和之前的时间做对比,最后记录下整个逻辑消耗的时间日志。
这种过程之后,相当于业务逻辑之中几乎没有非业务逻辑相关的内容了,依旧是原来简洁清爽的 hello。
这段代码在阅读上不是很清晰,他传入了一个 Handler 又返回了一个 Handler。本质上在执行一个具体的 http.Handler 的时候其实执行的是返回的函数逻辑,这个函数会先进行非业务的处理,然后执行业务逻辑,然后再执行业务逻辑。所以在刚刚提到的几个名称当中,其实最合适的还是叫做洋葱模式。
多层嵌套
如果我们还是按照这个思路把业务逻辑包裹起来,那么不仅可以包裹一层,还能包裹多层,比如除了打日志,还有超时、限流等,一层层地嵌套。
1customizeHandler = logger(timeout(ratelimit(helloHandler)))
每一个 middleware 的实现都是把原来的函数包装了一下,返回了新的函数。所以一个套一个的方式是可以完成的,并且我们在最后挂在在真实路由上的一定是最后的 Handler。
而这种多层嵌套结构在项目执行过程中的处理流程是这样的:
- 程序一开始会进入到最外层的 logger 的 middleware 的上半部分逻辑,执行完成之后会继续往内层的 middleware 执行,然后重复这种逻辑。然后就类似于剥洋葱一样,你是一层层地剥开,进入到最里面的业务逻辑。
- 在业务逻辑执行完成之后,会从这里返回,返回的过程还是类似剥洋葱一样,先执行最内层的 middleware 的下半部分逻辑,执行完成之后会继续往外层的 middleware 执行,不断重复这种逻辑。
- 执行到最外部 http response 的时候,相当于整个处理逻辑就处理完成了。
以上就是 web 框架中如何实现一个、多个 middleware 的完整逻辑流程。
Handler 原理
在上面的讲解中我们知道了 middleware 可以传入一个 Handler 然后返回一个 Handler。实现这种功能主要是 http.Handler 和 http.HandlerFunc,我们可以看一下源码结构:
1type Handler interface {
2 ServeHTTP(ResponseWriter, *Request)
3}
4
5type HandlerFunc func(ResponseWriter, *Request)
6
7func (f HanderFunc) ServeHTTP(w ResponseWriter, r *Request) {
8 f(w, r)
9}
http.Handler 它本质上是一个 interface,也就是说当我们的对象实现了 ServeHTTP 的函数功能,也就相当于实现了 Handler 的接口。
HandlerFunc 也是定义在 http 库中,它的签名就是场景的 http 的入口。
在内部是给 Handler 对象实现了 ServeHTTP,也就意味着我们可以返回一个 HandlerFunc 可以作为 http.Handler。虽然我们之前定义的 hello 本身不是 HandlerFunc,但它的签名和 HandlerFunc 的签名不一样,那么会经过临时匿名函数 func 的一个强制类型转换过程。
具体实现
我们有了这么多工具以后,如果代码依旧这么写:
1customizeHandler = logger(timeout(ratelimit(helloHandler)))
看起来不仅丑陋,还不好理解,所以我们大多数框架是这么实现的:
1r = NewRouter()
2r.Use(logger)
3r.Use(timeout)
4r.Use(ratelimit)
5r.Add("/", helloHandler)
这里的 Use 的源码实现也贴出来一下:
1type middleware func(http.Handler) http.Handler
2
3type Router struct {
4 middlewareChain [] middleware
5 mux map[string] http.Handler
6}
7
8func NewRouter() *Router {
9 return &Router {
10 mux: make(map[string]http.Handler),
11 }
12}
13
14func (r *Router) Use(m middleware) {
15 r.middlewareChain = append(r.middlewareChain, m)
16}
17
18func (r *Router) Add(route string, h http.Handler) {
19 var mergeHandler = h
20
21 for i := len(r.middlewareChain) - 1; i >= 0; i-- {
22 mergeHandler = r.middlewareChain[i](mergeHandler)
23 }
24
25 r.mux[route] = mergeHandler
26}
这段代码应该也可以直接用来执行。其中的原理是在写 Use 的时候,引用了 middleware 并且直接 append 到 middlewareChain 的数组中。并且把已经在middleware 里面的东西按照洋葱的包装方式,一层层地包装完成,最终把业务逻辑包装完成并且挂载到路由上。
OK,下期文章继续讲解 Router 的实现。