Skip to content

Commit

Permalink
fix middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
cch123 committed Dec 20, 2018
1 parent 6f083a9 commit 886a63b
Showing 1 changed file with 19 additions and 19 deletions.
38 changes: 19 additions & 19 deletions ch5-web/ch5-03-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ func main() {

```

每一个 handler 里都有之前提到的记录运行时间的代码,每次增加新的路由我们也同样需要把这些看起来长得差不多的代码拷贝到我们需要的地方去。因为代码不太多,所以实施起来也没有遇到什么大问题。
每一个handler里都有之前提到的记录运行时间的代码,每次增加新的路由我们也同样需要把这些看起来长得差不多的代码拷贝到我们需要的地方去。因为代码不太多,所以实施起来也没有遇到什么大问题。

渐渐的我们的系统增加到了 30 个路由和 handler 函数,每次增加新的 handler,我们的第一件工作就是把之前写的所有和业务逻辑无关的周边代码先拷贝过来。
渐渐的我们的系统增加到了30个路由和`handler`函数,每次增加新的handler,我们的第一件工作就是把之前写的所有和业务逻辑无关的周边代码先拷贝过来。

接下来系统安稳地运行了一段时间,突然有一天,老板找到你,我们最近找人新开发了监控系统,为了系统运行可以更加可控,需要把每个接口运行的耗时数据主动上报到我们的监控系统里。给监控系统起个名字吧,叫 metrics。现在你需要修改代码并把耗时通过 http post 的方式发给 metrics 了。我们来修改一下 helloHandler
接下来系统安稳地运行了一段时间,突然有一天,老板找到你,我们最近找人新开发了监控系统,为了系统运行可以更加可控,需要把每个接口运行的耗时数据主动上报到我们的监控系统里。给监控系统起个名字吧,叫metrics。现在你需要修改代码并把耗时通过HTTP Post的方式发给metrics 了。我们来修改一下helloHandler

```go
func helloHandler(wr http.ResponseWriter, r *http.Request) {
Expand All @@ -92,13 +92,13 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {
}
```

修改到这里,本能地发现我们的开发工作开始陷入了泥潭。无论未来对我们的这个 Web 系统有任何其它的非功能或统计需求,我们的修改必然牵一发而动全身。只要增加一个非常简单的非业务统计,我们就需要去几十个 handler 里增加这些业务无关的代码。虽然一开始我们似乎并没有做错,但是显然随着业务的发展,我们的行事方式让我们陷入了代码的泥潭。
修改到这里,本能地发现我们的开发工作开始陷入了泥潭。无论未来对我们的这个 Web 系统有任何其它的非功能或统计需求,我们的修改必然牵一发而动全身。只要增加一个非常简单的非业务统计,我们就需要去几十个handler里增加这些业务无关的代码。虽然一开始我们似乎并没有做错,但是显然随着业务的发展,我们的行事方式让我们陷入了代码的泥潭。

## 5.3.2 使用 middleware 剥离非业务逻辑

我们来分析一下,一开始在哪里做错了呢?我们只是一步一步地满足需求,把我们需要的逻辑按照流程写下去呀?

实际上,我们犯的最大的错误是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲,非业务的需求都是在 http 请求处理前做一些事情,或者/并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢?回到刚开头的例子,我们需要给我们的 helloHandler 增加超时时间统计,我们可以使用一种叫 `function adapter` 的方法来对 helloHandler 进行包装:
实际上,我们犯的最大的错误是把业务代码和非业务代码揉在了一起。对于大多数的场景来讲,非业务的需求都是在http请求处理前做一些事情,并且在响应完成之后做一些事情。我们有没有办法使用一些重构思路把这些公共的非业务功能代码剥离出去呢?回到刚开头的例子,我们需要给我们的`helloHandler()`增加超时时间统计,我们可以使用一种叫`function adapter`的方法来对`helloHandler()`进行包装:

```go

Expand All @@ -125,15 +125,15 @@ func main() {
}
```

这样就非常轻松地实现了业务与非业务之间的剥离,魔法就在于这个 timeMiddleware。可以从代码中看到,我们的 timeMiddleware 也是一个函数,其参数为 http.Handler,http.Handler 的定义在 net/http 包中:
这样就非常轻松地实现了业务与非业务之间的剥离,魔法就在于这个timeMiddleware。可以从代码中看到,我们的timeMiddleware也是一个函数,其参数为http.Handler,http.Handler的定义在`net/http`包中:

```go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
```

任何方法实现了 ServeHTTP,即是一个合法的 http.Handler,读到这里你可能会有一些混乱,我们先来梳理一下 http 库的 HandlerHandlerFuncServeHTTP 的关系:
任何方法实现了`ServeHTTP`,即是一个合法的`http.Handler`,读到这里你可能会有一些混乱,我们先来梳理一下http库的`Handler``HandlerFunc``ServeHTTP`的关系:

```go
type Handler interface {
Expand All @@ -147,19 +147,19 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
}

```
实际上只要你的 handler 函数签名是
实际上只要你的handler函数签名是

```go
func (ResponseWriter, *Request)
```

那么这个 handler 和 http.HandlerFunc 就有了一致的函数签名,可以将该 handler 函数进行类型转换,转为 http.HandlerFunc。而 http.HandlerFunc 实现了 http.Handler 这个接口。在 http 库需要调用你的 handler 函数来处理 http 请求时,会调用 HandlerFuncServeHTTP 函数,可见一个请求的基本调用链是这样的:
那么这个handler和`http.HandlerFunc()`就有了一致的函数签名,可以将该handler函数进行类型转换,转为`http.HandlerFunc`。而`http.HandlerFunc`实现了`http.Handler`这个接口。在http库需要调用你的handler函数来处理http请求时,会调用`HandlerFunc`的`ServeHTTP`函数,可见一个请求的基本调用链是这样的:

```go
h = getHandler() => h.ServeHTTP(w, r) => h(w, r)
```

上面提到的把自定义 handler 转换为 http.HandlerFunc 这个过程是必须的,因为我们的 handler 没有直接实现 ServeHTTP 这个接口。上面的代码中我们看到的 HandleFunc(注意 HandlerFunc 和 HandleFunc 的区别)里也可以看到这个强制转换过程:
上面提到的把自定义handler转换为`http.HandlerFunc`这个过程是必须的,因为我们的handler没有直接实现`ServeHTTP`这个接口。上面的代码中我们看到的HandleFunc(注意HandlerFunc和HandleFunc的区别)里也可以看到这个强制转换过程:

```go
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
Expand All @@ -173,9 +173,9 @@ func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Re
}
```

知道 handler 是怎么一回事,我们的中间件通过包装 handler,再返回一个新的 handler 就好理解了
知道handler是怎么一回事,我们的中间件通过包装handler,再返回一个新的handler就好理解了

总结一下,我们的中间件要做的事情就是通过一个或多个函数对 handler 进行包装,返回一个包括了各个中间件逻辑的函数链。我们把上面的包装再做得复杂一些:
总结一下,我们的中间件要做的事情就是通过一个或多个函数对handler进行包装,返回一个包括了各个中间件逻辑的函数链。我们把上面的包装再做得复杂一些:

```go
customizedHandler = logger(timeout(ratelimit(helloHandler)))
Expand Down Expand Up @@ -207,7 +207,7 @@ customizedHandler = logger(timeout(ratelimit(helloHandler)))

## 5.3.3 更优雅的 middleware 写法

上一节中解决了业务功能代码和非业务功能代码的解耦,但也提到了,看起来并不美观,如果需要修改这些函数的顺序,或者增删 middleware 还是有点费劲,本节我们来进行一些“写法”上的优化。
上一节中解决了业务功能代码和非业务功能代码的解耦,但也提到了,看起来并不美观,如果需要修改这些函数的顺序,或者增删中间件还是有点费劲,本节我们来进行一些“写法”上的优化。

看一个例子:

Expand All @@ -219,7 +219,7 @@ r.Use(ratelimit)
r.Add("/", helloHandler)
```

通过多步设置,我们拥有了和上一节差不多的执行函数链。胜在直观易懂,如果我们要增加或者删除 middleware,只要简单地增加删除对应的 Use 调用就可以了。非常方便。
通过多步设置,我们拥有了和上一节差不多的执行函数链。胜在直观易懂,如果我们要增加或者删除middleware,只要简单地增加删除对应的Use调用就可以了。非常方便。

从框架的角度来讲,怎么实现这样的功能呢?也不复杂:

Expand Down Expand Up @@ -250,12 +250,12 @@ func (r *Router) Add(route string, h http.Handler) {
}
```

注意代码中的 middleware 数组遍历顺序,和用户希望的调用顺序应该是"相反"的。应该不难理解。
注意代码中的middleware数组遍历顺序,和用户希望的调用顺序应该是"相反"的。应该不难理解。


## 5.3.4 哪些事情适合在 middleware 中做

以较流行的开源 golang 框架 chi 为例
以较流行的开源golang框架chi为例

```
compress.go
Expand All @@ -276,11 +276,11 @@ throttler.go
=> 通过定长大小的 channel 存储 token,并通过这些 token 对接口进行限流
```

每一个 Web 框架都会有对应的 middleware 组件,如果你有兴趣,也可以向这些项目贡献有用的 middleware,只要合理一般项目的维护人也愿意合并你的 pull request
每一个Web框架都会有对应的middleware组件,如果你有兴趣,也可以向这些项目贡献有用的middleware,只要合理一般项目的维护人也愿意合并你的Pull Request

比如开源界很火的 gin 这个框架,就专门为用户贡献的 middleware 开了一个仓库
比如开源界很火的gin这个框架,就专门为用户贡献的middleware开了一个仓库

![](../images/ch6-03-gin_contrib.png)

如果读者去阅读 gin 的源码的话,可能会发现 gin 的 middleware 中处理的并不是 http.Handler,而是一个叫 gin.HandlerFunc 的函数类型,和本节中讲解的 http.Handler 签名并不一样。不过实际上 gin 的 handler 也只是针对其框架的一种封装,middleware 的原理与本节中的说明是一致的
如果读者去阅读gin的源码的话,可能会发现gin的middleware中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的handler也只是针对其框架的一种封装,middleware的原理与本节中的说明是一致的

0 comments on commit 886a63b

Please sign in to comment.