携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情
前言
前情提要:
终于把专栏搞好啦,以后可以直接在专栏里看本系列的往期文章~
终于来到 HttpRouter “源码分析” 部分的收官之篇啦~ 本篇中,我们将把源码中剩下的所有内容全部讲完。当然,光看源码分析可能没啥感觉,再加上前面的文章很长,不知道看到这里的你是不是已经把前面的内容都忘光了呢?为了巩固我们学到的知识,并且提升对 HttpRouter 的运用能力,本系列还会再加更至少两篇的文章,分别来进行梳理总结,以及通过实际的案例来使用 HttpRouter 中的各项功能。
如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~
CleanPath
在上一章中,我们讲了大小写非敏感的查找,它是路由中 RedirectFixedPath
功能的一部分。实际上,在进行查找前,它先将请求路径化为了最简形式,也就是调用了 path.go
中的 CleanPath
函数。让我们来看一下吧~
源码
讲解
CleanPath
实际上就是URL版本的 path.Clean
,它会返回路径的最简洁形式。
它通过纯词法处理返回与传入路径 path
等价的最短路径名。它迭代应用下列规则,直到无法进行更进一步处理为止:
-
用一个斜杠替换多个斜杠;
-
删除每个
.
元素(当前目录); -
删除每个内部
..
元素(父目录)以及前面的非..
元素; -
消除以根路径开始的
..
元素:也就是说,在路径的开始处将/..
替换为/
。
如果处理后的结果是空字符串,则返回根路径 /
。
下面来看看代码:
if p == "" { // 空串为返回根路径 '/'
return "/"
}
n := len(p) // 长度
var buf []byte // 结果
r := 1 // 记录路径中要进行处理的下一个索引
w := 1 // 记录结果中要写入的下一个索引
// 可以看做是两个指针
if p[0] != '/' { // 路径必须以 '/' 开始
r = 0
buf = make([]byte, n+1)
buf[0] = '/'
}
trailing := n > 1 && p[n-1] == '/' // 标记是否有结尾斜杠
复制代码
向结果中添加字符是通过以下函数进行的,
// buf是结果缓存,s是原字符串,w是写入位置,c是要写入的字符
func bufApp(buf *[]byte, s string, w int, c byte) {
if *buf == nil { // buf 还未创建时
if s[w] == c { // 如果写入字符与原字符串在该位置字符相同,则直接返回
return
}
*buf = make([]byte, len(s)) // 否则创建buf
copy(*buf, s[:w]) // 将应该写入的部分写入
}
(*buf)[w] = c // 写入字符
}
复制代码
这个函数对结果缓存的创建有一个延迟作用,只有当必要时,也就是路径需要被化简时,才会创建缓存,并且将写入指针前面的路径(未修改的路径)直接拷贝过来,然后向写入位置写入字符。之后,这个函数就只会执行最后一句,与正常写入一样了。
接下来是遍历路径,进行化简。相比于 path
包,这里因为没有 lazybuf
而稍显笨重,但循环是完全内联的,所以它没有昂贵的函数调用(除了make以外)。这里要解释一下,为什么它是内联的。
运行下面的命令可以看到类似的结果:
$ go build -gcflags="-m -m" .\path.go
# command-line-arguments
.\path.go:114:6: can inline bufApp with cost 30 as: func(*[]byte, string, int, byte) { if *buf == nil { if s[w] == c { return }; *buf = make([]byte, len(s)); copy(*buf, s[:w]) }; (*buf)[w] = c }
.\path.go:21:6: cannot inline CleanPath: function too complex: cost 337 exceeds budget 80
.\path.go:88:11: inlining call to bufApp
.\path.go:94:11: inlining call to bufApp
.\path.go:103:9: inlining call to bufApp
复制代码
这里实际上 go 的编译器优化,对于在 AST 中节点数小于 80 个的函数会自动进行内联优化。所以 bufApp
就被自动内联到循环中了。
for r < n { // 从根/的下一个字符开始,每次判断是以到下一个/为止的片段进行的
switch {
case p[r] == '/': //前面已经有一个/了,再有/就要消除
r++ // r只负责标记下一个读到哪儿了,读完就前进
case p[r] == '.' && r+1 == n: // 以.结尾,因为本次就结束循环了,所以要通过循环外面的判断来添加尾斜杠
trailing = true
r++
case p[r] == '.' && p[r+1] == '/': // ./ 结构,直接跳过
r += 2
// ../或以..结尾的结构
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
r += 3
if w > 1 { // 回溯到上一个/的位置
w--
if buf == nil {
for w > 1 && p[w] != '/' { // 还没有创建buf就从路径参数里回溯
w--
}
} else {
for w > 1 && buf[w] != '/' { // 否则从buf里回溯
w--
}
}
}
default:
if w > 1 { // 为前一个路径片段的结尾添加斜杠
bufApp(&buf, p, w, '/')
w++
}
for r < n && p[r] != '/' { // 一直写到下一个斜杠(不包含)
bufApp(&buf, p, w, p[r])
w++
r++
}
}
}
复制代码
上面代码的主要逻辑是,不算根路径的 /
,从后面开始按照 /
来划分每一个路径片段,一次处理一个片段,在之后的循环时如果跳到 default 了,则添加前一个片段的 /
并把从前一个片段处理完到这一次循环之间处理的片段添加到 buf 中(不包括斜杠)。
if trailing && w > 1 {
bufApp(&buf, p, w, '/')
w++
}
if buf == nil {
return p[:w]
}
return string(buf[:w])
复制代码
最后就是判断一下要不要加尾斜杠,如果没有修改(或者只删除了结尾的一部分),那么直接返回原路径的(或者它前面的部分)就可以了,否则返回缓存中的结果。
allowed
源码
讲解
allowed
源码比较容易理解,就不详细的讲了,在这里梳理一下流程:
-
如果路径是
*
,说明是全局范围的查询:-
如果请求方法是空的,表示刷新全局对允许请求方法的缓存。
-
如果不为空,返回全局对允许请求方法的缓存。
-
-
如果不是全局查询:
- 遍历所有请求方法的路由树,除了传入的请求方法(因为请求已经失败了)和 OPTIONS 方法以外,通过
getValue
看是不是能获取到 handle,可以就说明该方法允许请求。
- 遍历所有请求方法的路由树,除了传入的请求方法(因为请求已经失败了)和 OPTIONS 方法以外,通过
-
最后如果允许的请求方法不为空,向其中添加 OPTIONS 后,将请求方法按升序排序,转为以逗号分隔的字符串并返回。
其它函数
剩下的函数中有一些是为 net/http
包的一些请求处理函数写的适配器:
func (r *Router) Handler(method, path string, handler http.Handler) {
r.Handle(method, path,
func(w http.ResponseWriter, req *http.Request, p Params) {
if len(p) > 0 {
ctx := req.Context()
ctx = context.WithValue(ctx, ParamsKey, p)
req = req.WithContext(ctx)
}
handler.ServeHTTP(w, req)
},
)
}
func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
r.Handler(method, path, handler)
}
复制代码
第一个是给 http.Handler
写的,第二个是给 http.HandlerFunc
写的,你可以用这两个函数快速重构用 http
包写的代码。
还有像 Lookup
函数可以手动查询某一路径,这主要是便于用来基于本包写一些高级的框架。
func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
if root := r.trees[method]; root != nil {
return root.getValue(path)
}
return nil, nil, false
}
复制代码
以及还有一些请求方法,之前提到了实际上和 GET
一样都是对 Handle
方法的调用。
HttpRouter 也提供了基本的静态文件访问服务,你注册的路径必须以 /*filepath
结尾。 如果想使用操作系统的文件系统实现,需要用 http.Dir
。
例如: router.ServeFiles("/src/*filepath", http.Dir("/var/www"))
。
func (r *Router) ServeFiles(path string, root http.FileSystem) {
if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
panic("path must end with /*filepath in path '" + path + "'")
}
fileServer := http.FileServer(root)
r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
req.URL.Path = ps.ByName("filepath")
fileServer.ServeHTTP(w, req)
})
}
复制代码
总结
到这里,HttpRouter 的全部源码我们就分析完了,完结撒花 ✿✿ヽ(°▽°)ノ✿ !
最后,如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~