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:
parent
886a63bb79
commit
b57eb9e36e
@ -105,19 +105,19 @@ r.PanicHandler = func(w http.ResponseWriter, r *http.Request, c interface{}) {
|
||||
|
||||
## 5.2.2 原理
|
||||
|
||||
httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。下图是一个典型的字典树结构:
|
||||
httprouter和众多衍生router使用的数据结构被称为压缩字典树(Radix Tree)。读者可能没有接触过压缩字典树,但对字典树(Trie Tree)应该有所耳闻。*图 5-1*是一个典型的字典树结构:
|
||||
|
||||

|
||||
|
||||
*图 6-1 字典树*
|
||||
*图 5-1 字典树*
|
||||
|
||||
字典树常用来进行字符串检索,例如用给定的字符串序列建立字典树。对于目标字符串,只要从根节点开始深度优先搜索,即可判断出该字符串是否曾经出现过,时间复杂度为O(n),n可以认为是目标字符串的长度。为什么要这样做?字符串本身不像数值类型可以进行数值比较,两个字符串对比的时间复杂度取决于字符串长度。如果不用字典树来完成上述功能,要对历史字符串进行排序,再利用二分查找之类的算法去搜索,时间复杂度只高不低。可认为字典树是一种空间换时间的典型做法。
|
||||
|
||||
普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。下图是典型的压缩字典树结构:
|
||||
普通的字典树有一个比较明显的缺点,就是每个字母都需要建立一个孩子节点,这样会导致字典树的层数比较深,压缩字典树相对好地平衡了字典树的优点和缺点。是典型的压缩字典树结构:
|
||||
|
||||

|
||||
|
||||
*图 6-2 压缩字典树*
|
||||
*图 5-2 压缩字典树*
|
||||
|
||||
每个节点上不只存储一个字母了,这也是压缩字典树中“压缩”的主要含义。使用压缩字典树可以减少树的层数,同时因为每个节点上数据存储也比通常的字典树要多,所以程序的局部性较好(一个节点的path加载到 cache 即可进行多个字符的对比),从而对CPU缓存友好。
|
||||
|
||||
@ -178,7 +178,7 @@ r.PUT("/user/installations/:installation_id/repositories/:reposit", Hello)
|
||||
|
||||

|
||||
|
||||
*图 6-3 插入路由之后的压缩字典树*
|
||||
*图 5-3 插入路由之后的压缩字典树*
|
||||
|
||||
radix的节点类型为`*httprouter.node`,为了说明方便,我们留下了目前关心的几个字段:
|
||||
|
||||
@ -201,19 +201,19 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild
|
||||
|
||||
### 5.2.3.2 子节点插入
|
||||
|
||||
当插入`GET /marketplace_listing/plans`时,类似前面PUT的过程,GET树的结构如*图 6-4*:
|
||||
当插入`GET /marketplace_listing/plans`时,类似前面PUT的过程,GET树的结构如*图 5-4*:
|
||||
|
||||

|
||||
|
||||
*图 6-4 插入第一个节点的压缩字典树*
|
||||
*图 5-4 插入第一个节点的压缩字典树*
|
||||
|
||||
因为第一个路由没有参数,path都被存储到根节点上了。所以只有一个节点。
|
||||
|
||||
然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见*图 6-5*:
|
||||
然后插入 `GET /marketplace_listing/plans/:id/accounts`,新的路径与之前的路径有共同的前缀,且可以直接在之前叶子节点后进行插入,那么结果也很简单,插入后的树结构见*图 5-5*:
|
||||
|
||||

|
||||
|
||||
*图 6-5 插入第二个节点的压缩字典树*
|
||||
*图 5-5 插入第二个节点的压缩字典树*
|
||||
|
||||
由于`:id`这个节点只有一个字符串的普通子节点,所以indices还依然不需要处理。
|
||||
|
||||
@ -221,19 +221,19 @@ indices: 子节点索引,当子节点为非参数类型,即本节点的 wild
|
||||
|
||||
### 5.2.3.3 边分裂
|
||||
|
||||
接下来我们插入`GET /search`,这时会导致树的边分裂,见*图 6-6*。
|
||||
接下来我们插入`GET /search`,这时会导致树的边分裂,见*图 5-6*。
|
||||
|
||||

|
||||
|
||||
*图 6-6 插入第三个节点,导致边分裂*
|
||||
*图 5-6 插入第三个节点,导致边分裂*
|
||||
|
||||
原有路径和新的路径在初始的`/`位置发生分裂,这样需要把原有的root节点内容下移,再将新路由 `search`同样作为子节点挂在root节点之下。这时候因为子节点出现多个,root节点的indices提供子节点索引,这时候该字段就需要派上用场了。"ms" 代表子节点的首字母分别为m(marketplace)和s(search)。
|
||||
|
||||
我们一口作气,把`GET /status`和`GET /support`也插入到树中。这时候会导致在`search`节点上再次发生分裂,最终结果见*图 6-7*:
|
||||
我们一口作气,把`GET /status`和`GET /support`也插入到树中。这时候会导致在`search`节点上再次发生分裂,最终结果见*图 5-7*:
|
||||
|
||||

|
||||
|
||||
*图 6-7 插入所有路由后的压缩字典树*
|
||||
*图 5-7 插入所有路由后的压缩字典树*
|
||||
|
||||
### 5.2.3.4 子节点冲突处理
|
||||
|
||||
|
@ -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) {
|
||||
@ -125,7 +125,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
这样就非常轻松地实现了业务与非业务之间的剥离,魔法就在于这个timeMiddleware。可以从代码中看到,我们的timeMiddleware也是一个函数,其参数为http.Handler,http.Handler的定义在`net/http`包中:
|
||||
这样就非常轻松地实现了业务与非业务之间的剥离,魔法就在于这个timeMiddleware。可以从代码中看到,我们的`timeMiddleware()`也是一个函数,其参数为`http.Handler`,`http.Handler`的定义在`net/http`包中:
|
||||
|
||||
```go
|
||||
type Handler interface {
|
||||
@ -181,10 +181,12 @@ func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Re
|
||||
customizedHandler = logger(timeout(ratelimit(helloHandler)))
|
||||
```
|
||||
|
||||
这个函数链在执行过程中的上下文可以用下面这张图来表示。
|
||||
这个函数链在执行过程中的上下文可以用*图 5-8*来表示。
|
||||
|
||||

|
||||
|
||||
*图 5-8 请求处理过程*
|
||||
|
||||
再直白一些,这个流程在进行请求处理的时候实际上就是不断地进行函数压栈再出栈,有一些类似于递归的执行流:
|
||||
|
||||
```
|
||||
@ -278,9 +280,11 @@ throttler.go
|
||||
|
||||
每一个Web框架都会有对应的middleware组件,如果你有兴趣,也可以向这些项目贡献有用的middleware,只要合理一般项目的维护人也愿意合并你的Pull Request。
|
||||
|
||||
比如开源界很火的gin这个框架,就专门为用户贡献的middleware开了一个仓库:
|
||||
比如开源界很火的gin这个框架,就专门为用户贡献的middleware开了一个仓库,见*图 5-9*:
|
||||
|
||||

|
||||
|
||||
*图 5-9 *
|
||||
|
||||
如果读者去阅读gin的源码的话,可能会发现gin的middleware中处理的并不是`http.Handler`,而是一个叫`gin.HandlerFunc`的函数类型,和本节中讲解的`http.Handler`签名并不一样。不过实际上gin的handler也只是针对其框架的一种封装,middleware的原理与本节中的说明是一致的。
|
||||
|
||||
|
@ -1,14 +1,16 @@
|
||||
# 5.4 validator 请求校验
|
||||
|
||||
社区里曾经有人用这张图来嘲笑 PHP:
|
||||
社区里曾经有人用*图 5-10*来嘲笑PHP:
|
||||
|
||||

|
||||
|
||||
实际上这是一个语言无关的场景,需要进行字段校验的情况有很多,Web系统的form/json提交只是一个典型的例子。我们用go来写一个类似上图的校验demo。然后研究怎么一步步对其进行改进。
|
||||
*图 5-10 validator 流程*
|
||||
|
||||
实际上这是一个语言无关的场景,需要进行字段校验的情况有很多,Web系统的Form/JSON提交只是一个典型的例子。我们用go来写一个类似上图的校验demo。然后研究怎么一步步对其进行改进。
|
||||
|
||||
## 5.4.1 重构请求校验函数
|
||||
|
||||
假设我们的数据已经通过某个 binding 库绑定到了具体的 struct 上。
|
||||
假设我们的数据已经通过某个binding库绑定到了具体的struct上。
|
||||
|
||||
```go
|
||||
type RegisterReq struct {
|
||||
@ -40,7 +42,7 @@ func register(req RegisterReq) error{
|
||||
}
|
||||
```
|
||||
|
||||
我们在 golang 里成功写出了 hadoken 开路的箭头型代码。。这种代码一般怎么进行优化呢?
|
||||
我们在golang里成功写出了hadoken开路的箭头型代码。。这种代码一般怎么进行优化呢?
|
||||
|
||||
很简单,在《重构》一书中已经给出了方案:[Guard Clauses](https://refactoring.com/catalog/replaceNestedConditionalWithGuardClauses.html)。
|
||||
|
||||
@ -67,13 +69,13 @@ func register(req RegisterReq) error{
|
||||
}
|
||||
```
|
||||
|
||||
代码更清爽,看起来也不那么别扭了。这是比较通用的重构理念。虽然使用了重构方法使我们的 validate 过程看起来优雅了,但我们还是得为每一个 http 请求都去写这么一套差不多的 validate 函数,有没有更好的办法来帮助我们解除这项体力劳动?答案就是 validator。
|
||||
代码更清爽,看起来也不那么别扭了。这是比较通用的重构理念。虽然使用了重构方法使我们的validate过程看起来优雅了,但我们还是得为每一个http请求都去写这么一套差不多的validate函数,有没有更好的办法来帮助我们解除这项体力劳动?答案就是validator。
|
||||
|
||||
## 5.4.2 用 validator 解放体力劳动
|
||||
|
||||
从设计的角度讲,我们一定会为每个请求都声明一个 struct。前文中提到的校验场景我们都可以通过 validator 完成工作。还以前文中的 struct 为例。为了美观起见,我们先把 json tag 省略掉。
|
||||
从设计的角度讲,我们一定会为每个请求都声明一个struct。前文中提到的校验场景我们都可以通过validator完成工作。还以前文中的struct为例。为了美观起见,我们先把json tag省略掉。
|
||||
|
||||
这里我们引入一个新的 validator 库:
|
||||
这里我们引入一个新的validator库:
|
||||
|
||||
> https://github.com/go-playground/validator
|
||||
|
||||
@ -104,7 +106,7 @@ func validate(req RegisterReq) error {
|
||||
|
||||
```
|
||||
|
||||
这样就不需要在每个请求进入业务逻辑之前都写重复的 validate 函数了。本例中只列出了这个 validator 非常简单的几个功能。
|
||||
这样就不需要在每个请求进入业务逻辑之前都写重复的validate函数了。本例中只列出了这个validator非常简单的几个功能。
|
||||
|
||||
我们试着跑一下这个程序,输入参数设置为:
|
||||
|
||||
@ -125,11 +127,11 @@ fmt.Println(err)
|
||||
// 'PasswordRepeat' failed on the 'eqfield' tag
|
||||
```
|
||||
|
||||
如果觉得这个 validator 提供的错误信息不够人性化,例如要把错误信息返回给用户,那就不应该直接显示英文了。可以针对每种 tag 进行错误信息定制,读者可以自行探索。
|
||||
如果觉得这个`validator`提供的错误信息不够人性化,例如要把错误信息返回给用户,那就不应该直接显示英文了。可以针对每种tag进行错误信息定制,读者可以自行探索。
|
||||
|
||||
## 5.4.3 原理
|
||||
|
||||
从结构上来看,每一个 struct 都可以看成是一棵树。假如我们有如下定义的 struct:
|
||||
从结构上来看,每一个struct都可以看成是一棵树。假如我们有如下定义的struct:
|
||||
|
||||
```go
|
||||
type Nested struct {
|
||||
@ -141,13 +143,15 @@ type T struct {
|
||||
}
|
||||
```
|
||||
|
||||
把这个 struct 画成一棵树:
|
||||
把这个struct画成一棵树,见*图 5-11*:
|
||||
|
||||

|
||||
|
||||
从字段校验的需求来讲,无论我们采用深度优先搜索还是广度优先搜索来对这棵 struct 树来进行遍历,都是可以的。
|
||||
*图 5-11 validator 树*
|
||||
|
||||
我们来写一个递归的深度优先搜索方式的遍历 demo:
|
||||
从字段校验的需求来讲,无论我们采用深度优先搜索还是广度优先搜索来对这棵struct树来进行遍历,都是可以的。
|
||||
|
||||
我们来写一个递归的深度优先搜索方式的遍历demo:
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -231,8 +235,8 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
这里我们简单地对 eq=x 和 email 这两个 tag 进行了支持,读者可以对这个程序进行简单的修改以查看具体的 validate 效果。为了演示精简掉了错误处理和复杂 case 的处理,例如 reflect.Int8/16/32/64,reflect.Ptr 等类型的处理,如果给生产环境编写 validate 库的话,请务必做好功能的完善和容错。
|
||||
这里我们简单地对`eq=x`和`email`这两个tag进行了支持,读者可以对这个程序进行简单的修改以查看具体的validate效果。为了演示精简掉了错误处理和复杂case的处理,例如reflect.Int8/16/32/64,reflect.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中做更精确的判断。
|
||||
|
||||
如果基于反射的 validator 真的成为了你服务的性能瓶颈怎么办?现在也有一种思路可以避免反射:使用 golang 内置的 parser 对源代码进行扫描,然后根据 struct 的定义生成校验代码。我们可以将所有需要校验的结构体放在单独的 package 内。这就交给读者自己去探索了。
|
||||
如果基于反射的validator真的成为了你服务的性能瓶颈怎么办?现在也有一种思路可以避免反射:使用golang内置的parser对源代码进行扫描,然后根据struct的定义生成校验代码。我们可以将所有需要校验的结构体放在单独的package内。这就交给读者自己去探索了。
|
||||
|
Loading…
x
Reference in New Issue
Block a user