Skip to content

Commit

Permalink
半角变全角
Browse files Browse the repository at this point in the history
  • Loading branch information
cch123 committed Dec 27, 2018
1 parent 2178202 commit 49bca18
Show file tree
Hide file tree
Showing 16 changed files with 150 additions and 152 deletions.
10 changes: 5 additions & 5 deletions ch5-web/ch5-01-introduction.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 5.1 Web 开发简介

因为Go的`net/http`包提供了基础的路由函数组合与丰富的功能函数。所以在社区里流行一种用Go编写API不需要框架的观点;在我们看来,如果你的项目的路由在个位数、URI固定且不通过URI来传递参数,那么确实使用官方库也就足够。但在复杂场景下,官方的http库还是有些力有不逮。例如下面这样的路由:
因为Go的`net/http`包提供了基础的路由函数组合与丰富的功能函数。所以在社区里流行一种用Go编写API不需要框架的观点在我们看来,如果你的项目的路由在个位数、URI固定且不通过URI来传递参数,那么确实使用官方库也就足够。但在复杂场景下,官方的http库还是有些力有不逮。例如下面这样的路由:

```
GET /card/:id
Expand All @@ -18,7 +18,7 @@ Go的Web框架大致可以分为这么两类:
1. Router框架
2. MVC类框架

在框架的选择上,大多数情况下都是依照个人的喜好和公司的技术栈。例如公司有很多技术人员是PHP出身,那么他们一定会非常喜欢像beego这样的框架,但如果公司有很多C程序员,那么他们的想法可能是越简单越好。比如很多大厂的C程序员甚至可能都会去用 C 去写很小的CGI程序,他们可能本身并没有什么意愿去学习MVC或者更复杂的Web框架,他们需要的只是一个非常简单的路由(甚至连路由都不需要,只需要一个基础的HTTP协议处理库来帮他省掉没什么意思的体力劳动)
在框架的选择上,大多数情况下都是依照个人的喜好和公司的技术栈。例如公司有很多技术人员是PHP出身,那么他们一定会非常喜欢像beego这样的框架,但如果公司有很多C程序员,那么他们的想法可能是越简单越好。比如很多大厂的C程序员甚至可能都会去用C语言去写很小的CGI程序,他们可能本身并没有什么意愿去学习MVC或者更复杂的Web框架,他们需要的只是一个非常简单的路由甚至连路由都不需要,只需要一个基础的HTTP协议处理库来帮他省掉没什么意思的体力劳动

Go的`net/http`包提供的就是这样的基础功能,写一个简单的`http echo server`只需要30s。

Expand Down Expand Up @@ -50,7 +50,7 @@ func main() {

```

如果你过了30s还没有完成这个程序,请检查一下你自己的打字速度是不是慢了(开个玩笑 :D)。这个例子是为了说明在Go中写一个HTTP协议的小程序有多么简单。如果你面临的情况比较复杂,例如几十个接口的企业级应用,直接用`net/http`库就显得不太合适了。
如果你过了30s还没有完成这个程序,请检查一下你自己的打字速度是不是慢了开个玩笑 :D。这个例子是为了说明在Go中写一个HTTP协议的小程序有多么简单。如果你面临的情况比较复杂,例如几十个接口的企业级应用,直接用`net/http`库就显得不太合适了。

我们来看看开源社区中一个Kafka监控项目中的做法:

Expand All @@ -69,7 +69,7 @@ func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
}
```

上面这段代码来自大名鼎鼎的linkedin公司的Kafka监控项目 Burrow,没有使用任何router框架,只使用了`net/http`。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的 URI,所以我们提供的服务就是下面这个样子:
上面这段代码来自大名鼎鼎的linkedin公司的Kafka监控项目Burrow,没有使用任何router框架,只使用了`net/http`。只看上面这段代码似乎非常优雅,我们的项目里大概只有这五个简单的URI,所以我们提供的服务就是下面这个样子:

```go
/
Expand Down Expand Up @@ -146,7 +146,7 @@ func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request
}
```

因为默认的`net/http`包中的mux不支持带参数的路由,所以Burrow这个项目使用了非常蹩脚的字符串 Split 和乱七八糟的 `switch case`来达到自己的目的,但实际上却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个handler函数逻辑上较简单,最复杂的也就是这个handleKafka。但实际上我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。
因为默认的`net/http`包中的`mux`不支持带参数的路由,所以Burrow这个项目使用了非常蹩脚的字符串`Split`和乱七八糟的 `switch case`来达到自己的目的,但实际上却让本来应该很集中的路由管理逻辑变得复杂,散落在系统的各处,难以维护和管理。如果读者细心地看过这些代码之后,可能会发现其它的几个`handler`函数逻辑上较简单,最复杂的也就是这个`handleKafka()`。但实际上我们的系统总是从这样微不足道的混乱开始积少成多,最终变得难以收拾。

根据我们的经验,简单地来说,只要你的路由带有参数,并且这个项目的API数目超过了10,就尽量不要使用`net/http`中默认的路由。在Go开源界应用最广泛的router是httpRouter,很多开源的router框架都是基于httpRouter进行一定程度的改造的成果。关于httpRouter路由的原理,会在本章节的router一节中进行详细的阐释。

Expand Down
32 changes: 16 additions & 16 deletions ch5-web/ch5-02-router.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 5.2 router 请求路由

在常见的Web框架中,router是必备的组件。Go语言圈子里router也时常被称为http的multiplexer。在上一节中我们通过对Burrow代码的简单学习,已经知道如何用http标准库中内置的mux来完成简单的路由功能了。如果开发Web系统对路径中带参数没什么兴趣的话,用http标准库中的mux就可以
在常见的Web框架中,router是必备的组件。Go语言圈子里router也时常被称为`http`的multiplexer。在上一节中我们通过对Burrow代码的简单学习,已经知道如何用`http`标准库中内置的mux来完成简单的路由功能了。如果开发Web系统对路径中带参数没什么兴趣的话,`http`标准库中的`mux`就可以

RESTful是几年前刮起的API设计风潮,在RESTful中除了GET和POST之外,还使用了http协议定义的几种其它的标准化语义。具体包括:
RESTful是几年前刮起的API设计风潮,在RESTful中除了GET和POST之外,还使用了HTTP协议定义的几种其它的标准化语义。具体包括:

```go
const (
Expand Down Expand Up @@ -30,9 +30,9 @@ PUT /user/starred/:owner/:repo
DELETE /user/starred/:owner/:repo
```

相信聪明的你已经猜出来了,这是github官方文档中挑出来的几个API设计。RESTful风格的API重度依赖请求路径。会将很多参数放在请求URI中。除此之外还会使用很多并不那么常见的HTTP状态码,不过本节只讨论路由,所以先略过不谈。
相信聪明的你已经猜出来了,这是Github官方文档中挑出来的几个API设计。RESTful风格的API重度依赖请求路径。会将很多参数放在请求URI中。除此之外还会使用很多并不那么常见的HTTP状态码,不过本节只讨论路由,所以先略过不谈。

如果我们的系统也想要这样的URI设计,使用标准库的mux显然就力不从心了
如果我们的系统也想要这样的URI设计,使用标准库的`mux`显然就力不从心了

## 5.2.1 httprouter

Expand All @@ -50,7 +50,7 @@ GET /user/info/:name
POST /user/:id
```

简单来讲的话,如果两个路由拥有一致的http method(指 GET/POST/PUT/DELETE)和请求路径前缀,且在某个位置出现了A路由是wildcard(指 :id 这种形式)参数,B路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接panic:
简单来讲的话,如果两个路由拥有一致的http方法(指 GET/POST/PUT/DELETE)和请求路径前缀,且在某个位置出现了A路由是wildcard(指:id这种形式)参数,B路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接panic:

```shell
panic: wildcard route ':id' conflicts with existing children in path '/user/:id'
Expand Down Expand Up @@ -81,7 +81,7 @@ Pattern: /src/*filepath
/src/subdir/somefile.go filepath = "subdir/somefile.go"
```

这种设计在RESTful中可能不太常见,主要是为了能够使用httprouter来做简单的http静态文件服务器
这种设计在RESTful中可能不太常见,主要是为了能够使用httprouter来做简单的HTTP静态文件服务器

除了正常情况下的路由支持,httprouter也支持对一些特殊情况下的回调函数进行定制,例如404的时候:

Expand All @@ -101,25 +101,25 @@ r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
}
```

目前开源界最为流行(star数最多)的Web框架[gin](https://github.com/gin-gonic/gin)使用的就是httprouter的变种。
目前开源界最为流行star数最多的Web框架[gin](https://github.com/gin-gonic/gin)使用的就是httprouter的变种。

## 5.2.2 原理

httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。*图 5-1*是一个典型的字典树结构:
httprouter和众多衍生router使用的数据结构被称为压缩字典树Radix Tree。读者可能没有接触过压缩字典树,但对字典树Trie Tree应该有所耳闻。*图 5-1*是一个典型的字典树结构:

![trie tree](../images/ch6-02-trie.png)

*图 5-1 字典树*

字典树常用来进行字符串检索,例如用给定的字符串序列建立字典树。对于目标字符串,只要从根节点开始深度优先搜索,即可判断出该字符串是否曾经出现过,时间复杂度为O(n),n可以认为是目标字符串的长度。为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。
字典树常用来进行字符串检索,例如用给定的字符串序列建立字典树。对于目标字符串,只要从根节点开始深度优先搜索,即可判断出该字符串是否曾经出现过,时间复杂度为`O(n)`,n可以认为是目标字符串的长度。为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。

普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。是典型的压缩字典树结构:

![radix tree](../images/ch6-02-radix.png)

*图 5-2 压缩字典树*

每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到 cache 即可进行多个字符的对比),从而对CPU缓存友好。
每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到cache即可进行多个字符的对比),从而对CPU缓存友好。

## 5.2.3 压缩字典树创建过程

Expand All @@ -138,7 +138,7 @@ GET /support
GET /marketplace_listing/plans/ohyes
```

最后一条补充路由是我们臆想的,除此之外所有API路由均来自于api.github.com。
最后一条补充路由是我们臆想的,除此之外所有API路由均来自于`api.github.com`

### 5.2.3.1 root 节点创建

Expand All @@ -153,7 +153,7 @@ type Router struct {
}
```

`trees`中的`key`即为http 1.1的RFC中定义的各种方法,具体有:
`trees`中的`key`即为HTTP 1.1的RFC中定义的各种方法,具体有:

```shell
GET
Expand Down Expand Up @@ -193,7 +193,7 @@ nType: 当前节点类型,有四个枚举值: 分别为 static/root/param/catc
param // 参数节点,例如 :id
catchAll // 通配符节点,例如 *anyway
indices: 子节点索引,当子节点为非参数类型,即本节点的 wildChild 为 false 时,会将每个子节点的首字母放在该索引数组。说是数组,实际上是个 string
indices子节点索引,当子节点为非参数类型,即本节点的wildChild为false时,会将每个子节点的首字母放在该索引数组。说是数组,实际上是个string
```

Expand All @@ -209,7 +209,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild

因为第一个路由没有参数,path都被存储到根节点上了。所以只有一个节点。

然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见*图 5-5*:
然后插入`GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见*图 5-5*:

![get radix step 2](../images/ch6-02-radix-get-2.png)

Expand All @@ -227,7 +227,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild

*图 5-6 插入第三个节点,导致边分裂*

原有路径和新的路径在初始的`/`位置发生分裂,这样需要把原有的root节点内容下移,再将新路由 `search`同样作为子节点挂在root节点之下。这时候因为子节点出现多个,root节点的indices提供子节点索引,这时候该字段就需要派上用场了。"ms" 代表子节点的首字母分别为m(marketplace)和s(search)
原有路径和新的路径在初始的`/`位置发生分裂,这样需要把原有的root节点内容下移,再将新路由 `search`同样作为子节点挂在root节点之下。这时候因为子节点出现多个,root节点的indices提供子节点索引,这时候该字段就需要派上用场了。"ms"代表子节点的首字母分别为mmarketplace)和s(search

我们一口作气,把`GET /status``GET /support`也插入到树中。这时候会导致在`search`节点上再次发生分裂,最终结果见*图 5-7*

Expand All @@ -237,7 +237,7 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild

### 5.2.3.4 子节点冲突处理

在路由本身只有字符串的情况下,不会发生任何冲突。只有当路由中含有wildcard(类似 :id)或者catchAll的情况下才可能冲突。这一点在前面已经提到了。
在路由本身只有字符串的情况下,不会发生任何冲突。只有当路由中含有wildcard类似 :id或者catchAll的情况下才可能冲突。这一点在前面已经提到了。

子节点的冲突处理很简单,分几种情况:

Expand Down
10 changes: 5 additions & 5 deletions ch5-web/ch5-03-middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func main() {

渐渐的我们的系统增加到了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,7 +92,7 @@ func helloHandler(wr http.ResponseWriter, r *http.Request) {
}
```

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

## 5.3.2 使用中间件剥离非业务逻辑

Expand Down Expand Up @@ -153,13 +153,13 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
func (ResponseWriter, *Request)
```

那么这个handler和`http.HandlerFunc()`就有了一致的函数签名,可以将该handler函数进行类型转换,转为`http.HandlerFunc`。而`http.HandlerFunc`实现了`http.Handler`这个接口。在http库需要调用你的handler函数来处理http请求时,会调用`HandlerFunc`的`ServeHTTP`函数,可见一个请求的基本调用链是这样的:
那么这个`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 Down Expand Up @@ -284,7 +284,7 @@ throttler.go

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

*图 5-9 *
*图 5-9 gin的中间件仓库*

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

Loading

0 comments on commit 49bca18

Please sign in to comment.