携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情
前言
前情提要(突然发现这个列表越来越长了,有时间开个专栏整合一下)
从本篇文章开始,我们要进入到路由是如何进行服务的代码分析了。通过对这些内容的分析,你将了解到 HttpRouter
中那些非常便捷、友好的功能是如何实现的,它们的执行顺序、流程和结果,以及在实际应用中,应该如何配置路由中的这些功能。
本篇文章将带你总览一下 ServeHttp
的流程,并重点讲解 getValue
路由查询的部分。如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~
ServeHttp
源码
讲解
第一篇中提到过了, Router
实际上就是对 http.Handler
接口的实现,所以它还需要实现接口中的 ServeHttp
方法。
该方法的流程如下:
-
如果路由中设置了
PanicHandler
,那么在发生运行时错误时,通过内置的recover
方法来终止失败的go程,并将错误信息传给handler,处理错误并使服务器免于崩溃; -
获取到请求路径后,先判断一下有没有该方法的路由树,有则执行查找:
-
通过
getValue
方法在路由中查找请求的路径,返回 handle,参数,以及tsr
标记;-
找到handle直接调用,结束。
-
未找到handle,如果不是代理请求
http.MethodConnect
且不是根路径:-
对于
GET
请求,设置状态301
,其它请求设置状态307
,这里是为了防止POST
被重定向到GET
上,但由于Go 1.3
不支持308
,所以就用307
了; -
如果路由中启用了
RedirectTrailingSlash
功能并且tsr
标记为true
,将自动添加或删除尾斜杠,然后重定向请求,结束。 -
如果路由中启用了
RedirectFixedPath
功能,将尝试修复路径,包括将路径化为最简洁的形式、进行大小写不敏感的查找,并可附带RedirectTrailingSlash
的操作(取决于是否启用了该功能);如果找到了就重定向请求,结束;如果没有则继续执行。
-
-
-
-
如果没有路由树,又分为两种情况:
-
如果请求方法是
http.MethodOptions
,且路由中启用了HandleOPTIONS
功能,那么通过allowed
方法找到该路径所有允许的请求方式(除了http.MethodOptions
以外);-
如果方式不为空:
-
如果路由中配置了
GlobalOPTIONS
(http.Handler
),调用它的ServeHTTP
方法来响应请求,结束。 -
如果没有,则直接结束。
-
-
如果方式为空,执行结尾的
404
处理。
-
-
如果路由中启用了
HandleMethodNotAllowed
功能来处理405
,也是通过allowed
方法找到除了该请求方式外,该路径其他所有允许的请求方式;-
如果不为空:
-
如果路由中配置了
MethodNotAllowed
(http.Handler
),调用它的ServeHTTP
方法来响应请求,结束。 -
如果没有,则使用默认调用
http.Error
方法来响应请求,结束。
-
-
如果方式为空,执行结尾的
404
处理。
-
-
-
如果上面的处理都不行,则进行
404
处理;如果路由中NotFound
(http.Handler
),调用它的ServeHTTP
方法来响应请求,否则,默认调用http.NotFound
, 结束。
以上就是路由进行服务的全部流程,可以看到所有的路由功能都包含在里面了,我把流程中的 结束 两个字都加粗了,为了让你注意到不是每次都执行所有功能的,并且对于 OPTIONS
请求,如果路由未配置 handler 可能会使请求无响应。
getValue
下面来详细讲一下,路由的核心功能 —— 查找。
getValue
用来在路由树中查找路径,返回其注册的 handle, 参数会被保存到一个映射中。如果没有找到对应的 handle,会进行 TSR (trailing slash redirect)
建议:如果查找的路径增加或删除尾斜杠后存在 handle,标记 tsr
为 true
。
源码
讲解
getValue
函数就是一个大循环,不断遍历传入的 path
直到结束。
整体上分为三种情况:
if len(path) > len(n.path) {
if path[:len(n.path)] == n.path {
// 如果未匹配路径比当前节点的路径长且这一部分的路径是匹配的...
}
}else if path == n.path {
// 已经到最后一个节点了,如果路径匹配的话...
}
未找到匹配路径,当以'/'结尾时(建议去掉'/'),或者当此时节点路径与未匹配路径只差一个尾斜杠且节点有 handle 时(建议加上'/'),建议进行 TSR
tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handle != nil)
return
第一种情况
接下来,让我们聚焦到最复杂的第一部分;根据是否有参数、参数类型、是否匹配,它又分为多种情况。
先更新一下未匹配的路径: path = path[len(n.path):]
,然后判断该向哪个方向走。
当节点的子节点不是参数节点时:
if !n.wildChild {
c := path[0]
for i := 0; i < len(n.indices); i++ { // 遍历子节点索引,找到则继续循环
if c == n.indices[i] {
n = n.children[i]
continue walk
}
}
// 没有找到,则当未匹配路径是'/'并且当前节点存在 handle,说明去掉尾斜杠可以匹配到当前节点
tsr = (path == "/" && n.handle != nil)
return
}
当前节点的子节点是参数节点时,获取子节点,n = n.children[0]
。
当节点是命名参数节点时,找到参数片段的结尾,并存储参数:
if p == nil { // 参数变量为空时,创建该变量
p = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // 在预分配容量内扩展切片
p[i].Key = n.path[1:] // 把':'去掉
p[i].Value = path[:end]
如果后面还有路径,当该节点有子节点时就继续循环,如果没有那么当路径以'/'结尾时,建议TSR:
if end < len(path) {
if len(n.children) > 0 {
path = path[end:]
n = n.children[0]
continue walk
}
tsr = (len(path) == end+1)
return
}
如果到这里刚好结束了,那么看一下是否存在 handle:
if handle = n.handle; handle != nil {
return
} else if len(n.children) == 1 {
// 如果当前节点有子节点为'/',且存在 handle,建议TSR(加上'/')
n = n.children[0]
tsr = (n.path == "/" && n.handle != nil)
}
当节点是任意捕获节点时,后面的都是参数,注意一下这里 p[i].Key = n.path[2:]
,任意捕获参数节点的路径是 '/*xxxx' 的格式,所以要去掉两个。
第二种情况
当路径应当匹配到最后一个节点时:
if handle = n.handle; handle != nil { // 存在handle
return
}
// 可能是注册了 /xxx/:xxx 和 /xxx ,而匹配的是 /xxx/ ,建议TSR(去掉'/')
if path == "/" && n.wildChild && n.nType != root {
tsr = true
return
}
// 如果当前节点的子节点为 '/' 或 '/*xxx' 且有handle,建议TSR(加上'/')
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
tsr = (len(n.path) == 1 && n.handle != nil) ||
(n.nType == catchAll && n.children[0].handle != nil)
return
}
}
return
总结
本篇文章带领大家总览了一下整个路由服务的流程,理解 HttpRouter
提供的各种功能的执行顺序,以及路由的各项配置之间的联系。然后,我们详细讲解了一个路由最重要的部分——查找,这一部分的代码本质上就是把所有的情况都充分地讨论了并且以合理的顺序进行处理。
在下一章中,我们将讲解另一种大小写非敏感的查找方法,那么之后基本上所有重要的内容也就都分析完了。
最后,如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~