1
0
mirror of https://github.com/chai2010/advanced-go-programming-book.git synced 2025-05-24 04:22:22 +00:00

restful -> RESTFul

This commit is contained in:
Xargin 2018-12-19 11:38:38 +08:00
parent 7d2936a8fb
commit 188c1b47e3

View File

@ -1,8 +1,8 @@
# 5.2 router 请求路由 # 5.2 router 请求路由
在常见的 Web 框架中router 是必备的组件。golang 圈子里 router 也时常被称为 http multiplexer。在上一节中我们通过对 Burrow 代码的简单学习,已经知道如何用 http 标准库中内置的 mux 来完成简单的路由功能了。如果开发 Web 系统对路径中带参数没什么兴趣的话,用 http 标准库中的 mux 就可以。 在常见的Web框架中router是必备的组件。golang圈子里router也时常被称为http的multiplexer。在上一节中我们通过对Burrow代码的简单学习已经知道如何用http标准库中内置的mux来完成简单的路由功能了。如果开发Web系统对路径中带参数没什么兴趣的话用http标准库中的mux就可以。
restful 是几年前刮起的 API 设计风潮,在 restful 中除了 GET 和 POST 之外,还使用了 http 协议定义的几种其它的标准化语义。具体包括: RESTful是几年前刮起的API设计风潮在RESTful中除了GET和POST之外还使用了http协议定义的几种其它的标准化语义。具体包括:
```go ```go
const ( const (
@ -18,7 +18,7 @@ const (
) )
``` ```
来看看 restful 中常见的请求路径: 来看看RESTful中常见的请求路径:
```shell ```shell
GET /repos/:owner/:repo/comments/:id/reactions GET /repos/:owner/:repo/comments/:id/reactions
@ -30,15 +30,15 @@ PUT /user/starred/:owner/:repo
DELETE /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 ## 5.2.1 httprouter
较流行的开源 golang Web 框架大多使用 httprouter或是基于 httprouter 的变种对路由进行支持。前面提到的 github 的参数式路由在 httprouter 中都是可以支持的。 较流行的开源go Web框架大多使用httprouter或是基于httprouter的变种对路由进行支持。前面提到的github的参数式路由在httprouter中都是可以支持的。
因为 httprouter 中使用的是显式匹配,所以在设计路由的时候需要规避一些会导致路由冲突的情况,例如: 因为httprouter中使用的是显式匹配所以在设计路由的时候需要规避一些会导致路由冲突的情况例如
``` ```
conflict: conflict:
@ -50,7 +50,7 @@ GET /user/info/:name
POST /user/:id POST /user/:id
``` ```
简单来讲的话,如果两个路由拥有一致的 http method (指 GET/POST/PUT/DELETE) 和请求路径前缀,且在某个位置出现了 A 路由是 wildcard (指 :id 这种形式) 参数B 路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接 panic 简单来讲的话如果两个路由拥有一致的http method(指 GET/POST/PUT/DELETE)和请求路径前缀且在某个位置出现了A路由是wildcard(指 :id 这种形式)参数B路由则是普通字符串那么就会发生路由冲突。路由冲突会在初始化阶段直接panic
```shell ```shell
panic: wildcard route ':id' conflicts with existing children in path '/user/:id' panic: wildcard route ':id' conflicts with existing children in path '/user/:id'
@ -69,9 +69,9 @@ main.main()
exit status 2 exit status 2
``` ```
还有一点需要注意,因为 httprouter 考虑到字典树的深度,在初始化时会对参数的数量进行限制,所以在路由中的参数数目不能超过 255否则会导致 httprouter 无法识别后续的参数。不过这一点上也不用考虑太多,毕竟 URI 是人设计且给人来看的,相信没有变态的 URI 能在一条路径中带有 200 个以上的参数。 还有一点需要注意因为httprouter考虑到字典树的深度在初始化时会对参数的数量进行限制所以在路由中的参数数目不能超过255否则会导致httprouter无法识别后续的参数。不过这一点上也不用考虑太多毕竟URI是人设计且给人来看的相信没有长得夸张的URI能在一条路径中带有200个以上的参数。
除支持路径中的 wildcard 参数之外httprouter 还可以支持 `*` 号来进行通配,不过 `*` 号开头的参数只能放在路由的结尾,例如下面这样: 除支持路径中的wildcard参数之外httprouter还可以支持`*`号来进行通配,不过`*`号开头的参数只能放在路由的结尾,例如下面这样:
```shell ```shell
Pattern: /src/*filepath Pattern: /src/*filepath
@ -81,9 +81,9 @@ Pattern: /src/*filepath
/src/subdir/somefile.go filepath = "subdir/somefile.go" /src/subdir/somefile.go filepath = "subdir/somefile.go"
``` ```
这种设计在 restful 中可能不太常见,主要是为了能够使用 httprouter 来做简单的 http 静态文件服务器。 这种设计在RESTful中可能不太常见主要是为了能够使用httprouter来做简单的http静态文件服务器。
除了正常情况下的路由支持httprouter 也支持对一些特殊情况下的回调函数进行定制,例如 404 的时候: 除了正常情况下的路由支持httprouter也支持对一些特殊情况下的回调函数进行定制例如404的时候
```go ```go
r := httprouter.New() r := httprouter.New()
@ -92,7 +92,7 @@ r.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
}) })
``` ```
或者内部 panic 的时候: 或者内部panic的时候
```go ```go
r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) { r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
log.Printf("Recovering from panic, Reason: %#v", c.(error)) log.Printf("Recovering from panic, Reason: %#v", c.(error))
@ -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 原理 ## 5.2.2 原理
httprouter 和众多衍生 router 使用的数据结构被称为 radix tree压缩字典树。读者可能没有接触过压缩字典树但对字典树 trie tree 应该有所耳闻。下图是一个典型的字典树结构: httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。下图是一个典型的字典树结构:
![trie tree](../images/ch6-02-trie.png) ![trie tree](../images/ch6-02-trie.png)
字典树常用来进行字符串检索,例如用给定的字符串序列建立字典树。对于目标字符串,只要从根节点开始深度优先搜索,即可判断出该字符串是否曾经出现过,时间复杂度为 O(n)n 可以认为是目标字符串的长度。为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。 字典树常用来进行字符串检索例如用给定的字符串序列建立字典树。对于目标字符串只要从根节点开始深度优先搜索即可判断出该字符串是否曾经出现过时间复杂度为O(n)n可以认为是目标字符串的长度。为什么要这样做字符串本身不像数值类型可以进行数值比较两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能要对历史字符串进行排序再利用二分查找之类的算法去搜索时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。
普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。下图是典型的压缩字典树结构: 普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。下图是典型的压缩字典树结构:
![radix tree](../images/ch6-02-radix.png) ![radix tree](../images/ch6-02-radix.png)
每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的 path 加载到 cache 即可进行多个字符的对比),从而对 CPU 缓存友好。 每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到 cache 即可进行多个字符的对比)从而对CPU缓存友好。
## 5.2.3 压缩字典树创建过程 ## 5.2.3 压缩字典树创建过程
我们来跟踪一下 httprouter 中,一个典型的压缩字典树的创建过程,路由设定如下: 我们来跟踪一下httprouter中一个典型的压缩字典树的创建过程路由设定如下
``` ```
PUT /user/installations/:installation_id/repositories/:repository_id PUT /user/installations/:installation_id/repositories/:repository_id
@ -134,11 +134,11 @@ GET /support
GET /marketplace_listing/plans/ohyes GET /marketplace_listing/plans/ohyes
``` ```
最后一条补充路由是我们臆想的,除此之外所有 API 路由均来自于 api.github.com。 最后一条补充路由是我们臆想的除此之外所有API路由均来自于api.github.com。
### 5.2.3.1 root 节点创建 ### 5.2.3.1 root 节点创建
httprouter 的 Router struct 中存储压缩字典树使用的是下述数据结构: httprouter的Router结构体中存储压缩字典树使用的是下述数据结构:
```go ```go
// 略去了其它部分的 Router struct // 略去了其它部分的 Router struct
@ -149,7 +149,7 @@ type Router struct {
} }
``` ```
trees 中的 key 即为 http 1.1 RFC 中定义的各种 method具体有 trees中的key即为http 1.1的RFC中定义的各种method具体有
```shell ```shell
GET GET
@ -161,19 +161,19 @@ PATCH
DELETE DELETE
``` ```
每一种 method 对应的都是一棵独立的压缩字典树这些树彼此之间不共享数据。具体到我们上面用到的路由PUT GET 是两棵树而非一棵。 每一种method对应的都是一棵独立的压缩字典树这些树彼此之间不共享数据。具体到我们上面用到的路由PUT和GET是两棵树而非一棵。
简单来讲,某个 method 第一次插入的路由就会导致对应字典树的根节点被创建,我们按顺序,先是一个 PUT 简单来讲某个method第一次插入的路由就会导致对应字典树的根节点被创建我们按顺序先是一个PUT
```go ```go
r := httprouter.New() r := httprouter.New()
r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello) r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello)
``` ```
这样 PUT 对应的根节点就会被创建出来。把这棵 PUT 的树画出来: 这样PUT对应的根节点就会被创建出来。把这棵PUT的树画出来
![put radix tree](../images/ch6-02-radix-put.png) ![put radix tree](../images/ch6-02-radix-put.png)
radix 的节点类型为 `*httprouter.node`,为了说明方便,我们留下了目前关心的几个字段: radix的节点类型为`*httprouter.node`,为了说明方便,我们留下了目前关心的几个字段:
``` ```
path: 当前节点对应的路径中的字符串 path: 当前节点对应的路径中的字符串
@ -190,45 +190,45 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild
``` ```
当然PUT 路由只有唯一的一条路径。接下来,我们以后续的多条 GET 路径为例,讲解子节点的插入过程。 当然PUT路由只有唯一的一条路径。接下来我们以后续的多条GET路径为例讲解子节点的插入过程。
### 5.2.3.2 子节点插入 ### 5.2.3.2 子节点插入
当插入 `GET /marketplace_listing/plans` 时,类似前面 PUT 的过程GET 树的结构如图所示: 当插入`GET /marketplace_listing/plans`类似前面PUT的过程GET树的结构如图所示
![get radix step 1](../images/ch6-02-radix-get-1.png) ![get radix step 1](../images/ch6-02-radix-get-1.png)
因为第一个路由没有参数path 都被存储到根节点上了。所以只有一个节点。 因为第一个路由没有参数path都被存储到根节点上了。所以只有一个节点。
然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后树变成了这样: 然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后树变成了这样:
![get radix step 2](../images/ch6-02-radix-get-2.png) ![get radix step 2](../images/ch6-02-radix-get-2.png)
由于 `:id` 这个节点只有一个字符串的普通子节点,所以 indices 还依然不需要处理。 由于`:id`这个节点只有一个字符串的普通子节点所以indices还依然不需要处理。
上面这种情况比较简单,新的路由可以直接作为原路由的子节点进行插入。实际情况不会这么美好。 上面这种情况比较简单,新的路由可以直接作为原路由的子节点进行插入。实际情况不会这么美好。
### 5.2.3.3 边分裂 ### 5.2.3.3 边分裂
接下来我们插入 `GET /search`,这时会导致树的边分裂。 接下来我们插入`GET /search`,这时会导致树的边分裂。
![get radix step 3](../images/ch6-02-radix-get-3.png) ![get radix step 3](../images/ch6-02-radix-get-3.png)
原有路径和新的路径在初始的 `/` 位置发生分裂,这样需要把原有的 root 节点内容下移,再将新路由 `search` 同样作为子节点挂在 root 节点之下。这时候因为子节点出现多个root 节点的 indices 提供子节点索引,这时候该字段就需要派上用场了。"ms" 代表子节点的首字母分别为 m(marketplace) s(search)。 原有路径和新的路径在初始的`/`位置发生分裂这样需要把原有的root节点内容下移再将新路由 `search`同样作为子节点挂在root节点之下。这时候因为子节点出现多个root节点的indices提供子节点索引这时候该字段就需要派上用场了。"ms" 代表子节点的首字母分别为m(marketplace)和s(search)。
我们一口作气,把 `GET /status` `GET /support` 也插入到树中。这时候会导致在 `search` 节点上再次发生分裂,来看看最终的结果: 我们一口作气,把`GET /status``GET /support`也插入到树中。这时候会导致在`search`节点上再次发生分裂,来看看最终的结果:
![get radix step 4](../images/ch6-02-radix-get-4.png) ![get radix step 4](../images/ch6-02-radix-get-4.png)
### 5.2.3.4 子节点冲突处理 ### 5.2.3.4 子节点冲突处理
在路由本身只有字符串的情况下,不会发生任何冲突。只有当路由中含有 wildcard(类似 :id) 或者 catchAll 的情况下才可能冲突。这一点在前面已经提到了。 在路由本身只有字符串的情况下不会发生任何冲突。只有当路由中含有wildcard(类似 :id)或者catchAll的情况下才可能冲突。这一点在前面已经提到了。
子节点的冲突处理很简单,分几种情况: 子节点的冲突处理很简单,分几种情况:
1. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 false。例如`GET /user/getAll` `GET /user/:id/getAddr`,或者 `GET /user/*aaa` `GET /user/:id` 1. 在插入wildcard节点时父节点的children数组非空且wildChild被设置为false。例如`GET /user/getAll``GET /user/:id/getAddr`,或者`GET /user/*aaa``GET /user/:id`
2. 在插入 wildcard 节点时,父节点的 children 数组非空且 wildChild 被设置为 true但该父节点的 wildcard 子节点要插入的 wildcard 名字不一样。例如:`GET /user/:id/info` `GET /user/:name/info` 2. 在插入wildcard节点时父节点的children数组非空且wildChild被设置为true但该父节点的wildcard子节点要插入的wildcard名字不一样。例如`GET /user/:id/info``GET /user/:name/info`
3. 在插入 catchAll 节点时,父节点的 children 非空。例如:`GET /src/abc` `GET /src/*filename`,或者 `GET /src/:id` `GET /src/*filename` 3. 在插入catchAll节点时父节点的children非空。例如`GET /src/abc``GET /src/*filename`,或者`GET /src/:id``GET /src/*filename`
4. 在插入 static 节点时,父节点的 wildChild 字段被设置为 true。 4. 在插入static节点时父节点的wildChild字段被设置为true。
5. 在插入 static 节点时,父节点的 children 非空,且子节点 nType catchAll。 5. 在插入static节点时父节点的children非空且子节点nType为catchAll。
只要发生冲突,都会在初始化的时候 panic。例如在插入我们臆想的路由`GET /marketplace_listing/plans/ohyes`出现第4种冲突情况它的父节点`marketplace_listing/plans/` wildChild 字段为 true。 只要发生冲突都会在初始化的时候panic。例如在插入我们臆想的路由`GET /marketplace_listing/plans/ohyes`出现第4种冲突情况它的父节点`marketplace_listing/plans/`的wildChild字段为true。