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

finish 5-4

This commit is contained in:
Xargin 2018-12-21 14:56:30 +08:00
parent 886a63bb79
commit b57eb9e36e
3 changed files with 41 additions and 33 deletions

View File

@ -105,19 +105,19 @@ r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
## 5.2.2 原理 ## 5.2.2 原理
httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。下图是一个典型的字典树结构: httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。*图 5-1*是一个典型的字典树结构:
![trie tree](../images/ch6-02-trie.png) ![trie tree](../images/ch6-02-trie.png)
*图 6-1 字典树* *图 5-1 字典树*
字典树常用来进行字符串检索例如用给定的字符串序列建立字典树。对于目标字符串只要从根节点开始深度优先搜索即可判断出该字符串是否曾经出现过时间复杂度为O(n)n可以认为是目标字符串的长度。为什么要这样做字符串本身不像数值类型可以进行数值比较两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能要对历史字符串进行排序再利用二分查找之类的算法去搜索时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。 字典树常用来进行字符串检索例如用给定的字符串序列建立字典树。对于目标字符串只要从根节点开始深度优先搜索即可判断出该字符串是否曾经出现过时间复杂度为O(n)n可以认为是目标字符串的长度。为什么要这样做字符串本身不像数值类型可以进行数值比较两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能要对历史字符串进行排序再利用二分查找之类的算法去搜索时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。
普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。下图是典型的压缩字典树结构: 普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。是典型的压缩字典树结构:
![radix tree](../images/ch6-02-radix.png) ![radix tree](../images/ch6-02-radix.png)
*图 6-2 压缩字典树* *图 5-2 压缩字典树*
每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到 cache 即可进行多个字符的对比)从而对CPU缓存友好。 每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到 cache 即可进行多个字符的对比)从而对CPU缓存友好。
@ -178,7 +178,7 @@ r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello)
![put radix tree](../images/ch6-02-radix-put.png) ![put radix tree](../images/ch6-02-radix-put.png)
*图 6-3 插入路由之后的压缩字典树* *图 5-3 插入路由之后的压缩字典树*
radix的节点类型为`*httprouter.node`,为了说明方便,我们留下了目前关心的几个字段: radix的节点类型为`*httprouter.node`,为了说明方便,我们留下了目前关心的几个字段:
@ -201,19 +201,19 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild
### 5.2.3.2 子节点插入 ### 5.2.3.2 子节点插入
当插入`GET /marketplace_listing/plans`类似前面PUT的过程GET树的结构如*图 6-4* 当插入`GET /marketplace_listing/plans`类似前面PUT的过程GET树的结构如*图 5-4*
![get radix step 1](../images/ch6-02-radix-get-1.png) ![get radix step 1](../images/ch6-02-radix-get-1.png)
*图 6-4 插入第一个节点的压缩字典树* *图 5-4 插入第一个节点的压缩字典树*
因为第一个路由没有参数path都被存储到根节点上了。所以只有一个节点。 因为第一个路由没有参数path都被存储到根节点上了。所以只有一个节点。
然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见*图 6-5*: 然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见*图 5-5*:
![get radix step 2](../images/ch6-02-radix-get-2.png) ![get radix step 2](../images/ch6-02-radix-get-2.png)
*图 6-5 插入第二个节点的压缩字典树* *图 5-5 插入第二个节点的压缩字典树*
由于`:id`这个节点只有一个字符串的普通子节点所以indices还依然不需要处理。 由于`:id`这个节点只有一个字符串的普通子节点所以indices还依然不需要处理。
@ -221,19 +221,19 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild
### 5.2.3.3 边分裂 ### 5.2.3.3 边分裂
接下来我们插入`GET /search`,这时会导致树的边分裂,见*图 6-6*。 接下来我们插入`GET /search`,这时会导致树的边分裂,见*图 5-6*。
![get radix step 3](../images/ch6-02-radix-get-3.png) ![get radix step 3](../images/ch6-02-radix-get-3.png)
*图 6-6 插入第三个节点,导致边分裂* *图 5-6 插入第三个节点,导致边分裂*
原有路径和新的路径在初始的`/`位置发生分裂这样需要把原有的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`节点上再次发生分裂,最终结果见*图 6-7* 我们一口作气,把`GET /status``GET /support`也插入到树中。这时候会导致在`search`节点上再次发生分裂,最终结果见*图 5-7*
![get radix step 4](../images/ch6-02-radix-get-4.png) ![get radix step 4](../images/ch6-02-radix-get-4.png)
*图 6-7 插入所有路由后的压缩字典树* *图 5-7 插入所有路由后的压缩字典树*
### 5.2.3.4 子节点冲突处理 ### 5.2.3.4 子节点冲突处理

View File

@ -79,7 +79,7 @@ func main() {
渐渐的我们的系统增加到了30个路由和`handler`函数每次增加新的handler我们的第一件工作就是把之前写的所有和业务逻辑无关的周边代码先拷贝过来。 渐渐的我们的系统增加到了30个路由和`handler`函数每次增加新的handler我们的第一件工作就是把之前写的所有和业务逻辑无关的周边代码先拷贝过来。
接下来系统安稳地运行了一段时间突然有一天老板找到你我们最近找人新开发了监控系统为了系统运行可以更加可控需要把每个接口运行的耗时数据主动上报到我们的监控系统里。给监控系统起个名字吧叫metrics。现在你需要修改代码并把耗时通过HTTP Post的方式发给metrics 了。我们来修改一下helloHandler 接下来系统安稳地运行了一段时间突然有一天老板找到你我们最近找人新开发了监控系统为了系统运行可以更加可控需要把每个接口运行的耗时数据主动上报到我们的监控系统里。给监控系统起个名字吧叫metrics。现在你需要修改代码并把耗时通过HTTP Post的方式发给metrics 了。我们来修改一下`helloHandler()`
```go ```go
func helloHandler(wr http.ResponseWriter, r *http.Request) { func helloHandler(wr http.ResponseWriter, r *http.Request) {
@ -125,7 +125,7 @@ func main() {
} }
``` ```
这样就非常轻松地实现了业务与非业务之间的剥离魔法就在于这个timeMiddleware。可以从代码中看到我们的timeMiddleware也是一个函数其参数为http.Handlerhttp.Handler的定义在`net/http`包中: 这样就非常轻松地实现了业务与非业务之间的剥离魔法就在于这个timeMiddleware。可以从代码中看到我们的`timeMiddleware()`也是一个函数,其参数为`http.Handler``http.Handler`的定义在`net/http`包中:
```go ```go
type Handler interface { type Handler interface {
@ -181,10 +181,12 @@ func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Re
customizedHandler = logger(timeout(ratelimit(helloHandler))) customizedHandler = logger(timeout(ratelimit(helloHandler)))
``` ```
这个函数链在执行过程中的上下文可以用下面这张图来表示。 这个函数链在执行过程中的上下文可以用*图 5-8*来表示。
![](../images/ch6-03-middleware_flow.png) ![](../images/ch6-03-middleware_flow.png)
*图 5-8 请求处理过程*
再直白一些,这个流程在进行请求处理的时候实际上就是不断地进行函数压栈再出栈,有一些类似于递归的执行流: 再直白一些,这个流程在进行请求处理的时候实际上就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
``` ```
@ -278,9 +280,11 @@ throttler.go
每一个Web框架都会有对应的middleware组件如果你有兴趣也可以向这些项目贡献有用的middleware只要合理一般项目的维护人也愿意合并你的Pull Request。 每一个Web框架都会有对应的middleware组件如果你有兴趣也可以向这些项目贡献有用的middleware只要合理一般项目的维护人也愿意合并你的Pull Request。
比如开源界很火的gin这个框架就专门为用户贡献的middleware开了一个仓库 比如开源界很火的gin这个框架就专门为用户贡献的middleware开了一个仓库,见*图 5-9*
![](../images/ch6-03-gin_contrib.png) ![](../images/ch6-03-gin_contrib.png)
*图 5-9 *
如果读者去阅读gin的源码的话可能会发现gin的middleware中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的handler也只是针对其框架的一种封装middleware的原理与本节中的说明是一致的。 如果读者去阅读gin的源码的话可能会发现gin的middleware中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的handler也只是针对其框架的一种封装middleware的原理与本节中的说明是一致的。

View File

@ -1,10 +1,12 @@
# 5.4 validator 请求校验 # 5.4 validator 请求校验
社区里曾经有人用这张图来嘲笑 PHP 社区里曾经有人用*图 5-10*来嘲笑PHP
![validate 流程](../images/ch6-04-validate.jpg) ![validate 流程](../images/ch6-04-validate.jpg)
实际上这是一个语言无关的场景需要进行字段校验的情况有很多Web系统的form/json提交只是一个典型的例子。我们用go来写一个类似上图的校验demo。然后研究怎么一步步对其进行改进。 *图 5-10 validator 流程*
实际上这是一个语言无关的场景需要进行字段校验的情况有很多Web系统的Form/JSON提交只是一个典型的例子。我们用go来写一个类似上图的校验demo。然后研究怎么一步步对其进行改进。
## 5.4.1 重构请求校验函数 ## 5.4.1 重构请求校验函数
@ -125,7 +127,7 @@ fmt.Println(err)
// 'PasswordRepeat' failed on the 'eqfield' tag // 'PasswordRepeat' failed on the 'eqfield' tag
``` ```
如果觉得这个 validator 提供的错误信息不够人性化,例如要把错误信息返回给用户,那就不应该直接显示英文了。可以针对每种 tag 进行错误信息定制,读者可以自行探索。 如果觉得这个`validator`提供的错误信息不够人性化例如要把错误信息返回给用户那就不应该直接显示英文了。可以针对每种tag进行错误信息定制读者可以自行探索。
## 5.4.3 原理 ## 5.4.3 原理
@ -141,10 +143,12 @@ type T struct {
} }
``` ```
把这个 struct 画成一棵树: 把这个struct画成一棵树,见*图 5-11*
![struct-tree](../images/ch6-04-validate-struct-tree.png) ![struct-tree](../images/ch6-04-validate-struct-tree.png)
*图 5-11 validator 树*
从字段校验的需求来讲无论我们采用深度优先搜索还是广度优先搜索来对这棵struct树来进行遍历都是可以的。 从字段校验的需求来讲无论我们采用深度优先搜索还是广度优先搜索来对这棵struct树来进行遍历都是可以的。
我们来写一个递归的深度优先搜索方式的遍历demo 我们来写一个递归的深度优先搜索方式的遍历demo
@ -231,7 +235,7 @@ func main() {
} }
``` ```
这里我们简单地对 eq=x 和 email 这两个 tag 进行了支持,读者可以对这个程序进行简单的修改以查看具体的 validate 效果。为了演示精简掉了错误处理和复杂 case 的处理,例如 reflect.Int8/16/32/64reflect.Ptr 等类型的处理,如果给生产环境编写 validate 库的话,请务必做好功能的完善和容错。 这里我们简单地对`eq=x``email`这两个tag进行了支持读者可以对这个程序进行简单的修改以查看具体的validate效果。为了演示精简掉了错误处理和复杂case的处理例如reflect.Int8/16/32/64reflect.Ptr等类型的处理如果给生产环境编写validate库的话请务必做好功能的完善和容错。
在前一小节中介绍的validator组件在功能上要远比我们这里的demo复杂的多。但原理很简单就是用reflect对struct进行树形遍历。有心的读者这时候可能会产生一个问题我们对struct进行validate时大量使用了reflect而go的reflect在性能上不太出众有时甚至会影响到我们程序的性能。这样的考虑确实有一些道理但需要对struct进行大量校验的场景往往出现在Web服务这里并不一定是程序的性能瓶颈所在实际的效果还是要从pprof中做更精确的判断。 在前一小节中介绍的validator组件在功能上要远比我们这里的demo复杂的多。但原理很简单就是用reflect对struct进行树形遍历。有心的读者这时候可能会产生一个问题我们对struct进行validate时大量使用了reflect而go的reflect在性能上不太出众有时甚至会影响到我们程序的性能。这样的考虑确实有一些道理但需要对struct进行大量校验的场景往往出现在Web服务这里并不一定是程序的性能瓶颈所在实际的效果还是要从pprof中做更精确的判断。