Open geektutu opened 5 years ago
very good!
it's cool
开始看不懂了。。。。
这里有问题, getRoutes 的时候有用到的 root.travel 并没有说出来 ,测试的时候有点懵逼,后来看github源码才发现
只有第二个参数接受:匹配吗
这一节难度大了不止一点半点 建议分两段 放后面讲
@Arbusz @zjfsdnu 为了降低代码量,这一节实现的路由算法很粗糙。因为整个系列的核心还是想介绍一个框架的最基础的构成,所以都只能是浅尝辄止,不过7天就打不住了。
Trie树有点懵了,那两个mathch函数和interst、search函数注释能多点就好了,看起来太费劲了。
这章确实看的懵逼了,注释太少,跨度太大。在慢慢研究了。
大佬写的太棒了,very cool
感谢大佬让我 如沐春风 如淋甘露 飘飘欲仙 让我走出人生低谷 让我让重新开始热爱生活
兔兔你好,发现了两个小bug
在 trie.go
里的
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
n.pattern = pattern
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil {
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
child.insert(pattern, parts, height+1)
}
如果第一次插入的pattern
为/:age
,method
为 GET
,handlefunc
为 handleAge()
那么会生成一个这样的 node
nodeAge = node{
pattern: ":age",
part: ":age",
children: nil,
isWild: true,
}
handlers["GET-:age"] = handleAge
第二次插入的 pattern
为 /18
,method
与第一次相同,仍然为 GET
,handlefunc
为 handle18()
,此时并不会修改之前的 node
,而是修改了之前 nodeAge
的 pattern
。
nodeAge = node{
pattern: "18",
part: ":age",
children: nil,
isWild: true,
}
handlers["GET-:age"] = handleAge
handlers["GET-18"] = handle18
接下来看看handle()
函数
func (r *router) handle(c *Context) {
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
c.Params = params
key := c.Method + "-" + n.pattern
r.handlers[key](c)
} else {
c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
}
}
当有一个 /19
的请求到来时,将会匹配到 nodeAge
,但是由于 nodeAge
的 pattern
变成了 18
,因此将会被 handle18()
处理,这不太合适。
GIN
的做法是将冲突的路由直接panic
了。
router
的 handle
方法貌似不是协程安全的
假设此时只有一个/:age
的router
nodeAge = node{
pattern: ":age",
part: ":age",
children: nil,
isWild: true,
}
handlers["GET-:age"] = handleAge
此时有两个请求地址分别为 /18
和 /19
的请求到达。
handle()
内的 getRoute()
函数得到的 params
分别为 key:age value:18
和 key:age value:19
,它们会对同一个 context
进行写入,因此不太安全。
在 http
包内的 server.go
文件中的 Serve
函数内,有这样一段注释
// Serve accepts incoming connections on the Listener l, creating a
// new service goroutine for each. The service goroutines read requests and
// then call srv.Handler to reply to them.
//
// HTTP/2 support is only enabled if the Listener returns *tls.Conn
// connections and they were configured with "h2" in the TLS
// Config.NextProtos.
//
// Serve always returns a non-nil error and closes l.
// After Shutdown or Close, the returned error is ErrServerClosed.
func (srv *Server) Serve(l net.Listener) error
可见,http.ListenAndServe()
是有可能开启多个协程的。
@liweiforeveryoung 非常感谢,指出了非常关键的问题。
第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。
第二个问题,http 请求是并发的,但每一个请求都会调用 ServeHTTP
,这个方法中,context 每次都创建新的,不会对同一个context进行写入。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := newContext(w, req)
engine.router.handle(c)
}
@geektutu @liweiforeveryoung 非常感谢,指出了非常关键的问题。
第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用
ServeHTTP
,这个方法中,context 每次都创建新的,不会对同一个context进行写入。func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
嗯,我刚刚看发现了ServeHTTP
每次都创建了一个新的context
,刚准备修改评论的,结果看见你回复了。
@geektutu @liweiforeveryoung 非常感谢,指出了非常关键的问题。
第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用
ServeHTTP
,这个方法中,context 每次都创建新的,不会对同一个context进行写入。func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
@geektutu @liweiforeveryoung 非常感谢,指出了非常关键的问题。
第一个bug,存在覆盖的问题,gin 的做法才是对的,应该把问题暴露给用户。 第二个问题,http 请求是并发的,但每一个请求都会调用
ServeHTTP
,这个方法中,context 每次都创建新的,不会对同一个context进行写入。func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { c := newContext(w, req) engine.router.handle(c) }
非常感谢作者的付出!!
问一个比较蠢的问题。search函数中调用了matchChildren,matchChildren什么情况下会有多个匹配返回呢?
建议这一章先把代码写一遍 大概清楚每一个方法是做什么的,然后按照 addRouter -> getRouter -> handle 在看一遍代码 走一遍流程就明了多了, 说白了就是 用前缀树结构存 前缀树结构取, 用GET /a/asd/c || GET a/s/c 匹配到路由(GET-/a/:param/c)对应的HandlerFunc 并把asd || s 存在context的Params里.
感谢楼主
func (n *node) travel(list *([]*node)) {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
child.travel(list)
}
}
func (n *node) String() string {
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
以上两个方法,在文章中没有提到,但是用到了,看了GitHub源码,才知道,建议后期这种不重要的代码可以提一下,要不会以为自己哪里漏了。
@BigYoungs 感谢建议~
其实自己在关键的地方多加点fmt就看懂怎么回事儿了 不过还是建议作者多讲讲 毕竟是从零实现系列。。。
感谢兔兔,从这里开始的4个方法matchChild,matchChildren,insert,search,设计方法和应用场景能简单说明下就更好了
@limaoxiaoer @xlban163 感谢二位的建议,这里可能假定了大家是计算机专业,学习过二叉树、B+树等常用的数据结构了。以后的文章细节会多写一些。
没其他语言开发基础的, 估计已经懵了, 建议小白们, 多看几遍, 每个方法仔细推敲一下, 也很好理解, 不必完全明白, 懂流程就行了, 继续往前看
isWild为true时表示的是通配符匹配吧?所以isWild为true时应该是模糊匹配吧 通配符匹配==精准匹配吗
写的太好了,十分感谢大佬的付出!
@Howard0o0 通配符匹是文中的模糊匹配,精准匹配是指每个字符都一致,而不是用通配符。
写的太好了,十分感谢大佬的付出!
@wushh @wangzukang 感谢支持,笔芯~
受益匪浅,感谢前辈的付出!
谢谢博主,我有小问题, 在Demo例子里 curl "http://localhost:9999/hello/geektutu/a/b", 匹配不了/hello/geektutu,但可以匹配到/hello 所有是否可以修改为:
func (n *node) search(parts []string, height int) *node {
if len(parts) == height || strings.HasPrefix(n.part, "*") {
if n.pattern == "" {
return nil
}
return n
}
part := parts[height]
children := n.matchChildren(part)
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
return result
}
}
//应该新增, 判断当前节点是否满足其前缀
if n.pattern != nil {
return n
}
return nil
}
很棒的学习资料!
有一个问题 get /a/:b get /a/c
/a/x 也是去到 /a/c
get /a/c get /a/:b 和 get /a/:b get /a/c 顺序不一样, 结果是两个不同的树
@GaloisZhou 嗯,前面评论提到了,这一块路由有冲突时没有提示,直接覆盖了。gin 的做法是直接 panic 报失败。
麻烦请问下单元测试的时候出现 missing ... in args forwarded to printf-like function 地方在c.Writer.Write([]byte(fmt.Sprintf(format, values)))这句代码中 输出结果会出现 %!s(MISSING)
@GaloisZhou 很棒的学习资料!
有一个问题 get /a/:b get /a/c
/a/x 也是去到 /a/c
get /a/c get /a/:b 和 get /a/:b get /a/c 顺序不一样, 结果是两个不同的树
学习中,有同样困惑,这个教程可能偏向思想。要想建立正确trie树,并且结果也正确可以这样处理:
1、 // 第一个匹配成功的节点,用于插入 func (n node) matchChild(part string) node { for _, child := range n.children { // 修改点:动态匹配做强校验,防止路由注册覆盖 if child.part == part || ((part[0] == ':' || part[0] == '*') && child.isWild) { return child } } return nil }
2、 // 所有匹配成功的节点,用于查找 func (n node) matchChildren(part string) []node { nodes := make([]node, 0) wildNodes := make([]node, 0) for _, child := range n.children { // 修改点:静态路由节点优先,动态路由节点延后 if child.part == part { nodes = append(nodes, child) } else if child.isWild { wildNodes = append(wildNodes, child) } } nodes = append(nodes, wildNodes...) return nodes }
大佬好、想问下 这种嵌套指针类型的结构体、 有什么好的办法 可以打印出结构体的具体数据内容呢
@feixintianxia 谢谢博主,我有小问题, 在Demo例子里 curl "http://localhost:9999/hello/geektutu/a/b", 匹配不了/hello/geektutu,但可以匹配到/hello 所有是否可以修改为:
func (n *node) search(parts []string, height int) *node { if len(parts) == height || strings.HasPrefix(n.part, "*") { if n.pattern == "" { return nil } return n } part := parts[height] children := n.matchChildren(part) for _, child := range children { result := child.search(parts, height+1) if result != nil { return result } } //应该新增, 判断当前节点是否满足其前缀 if n.pattern != nil { return n } return nil }
可以匹配到/hello是因为demo里面添加了/hello的路由,而不是因为/hello/geektutu
@yufeifly 问一个比较蠢的问题。search函数中调用了matchChildren,matchChildren什么情况下会有多个匹配返回呢?
这个问题我也想知道,大概是添加了这两个路由/hello/:name, /hello/tutu. 然后访问/hello/tutu的时候,就会返回多个匹配了吧。 也就是前面评论说的路由冲突?(还没看gin的源码,会不会就是matchChildren返回多个的时候,直接panic呢?
@whitebluepants
@yufeifly 问一个比较蠢的问题。search函数中调用了matchChildren,matchChildren什么情况下会有多个匹配返回呢?
这个问题我也想知道,大概是添加了这两个路由/hello/:name, /hello/tutu. 然后访问/hello/tutu的时候,就会返回多个匹配了吧。 也就是前面评论说的路由冲突?(还没看gin的源码,会不会就是matchChildren返回多个的时候,直接panic呢?
方法matchChildren中的判断在/hello/tutu和/hello/:name中都会append呢.(同样没看过gin源码,如果有看过这段源码的小伙伴,请指导一下,谢谢.)
if child.part == part || child.isWild { nodes = append(nodes, child) }
print大法有点好用
对比了一下github的代码,在trie.go里多了一个方法travel,在router.go里多了一个getRouters的方法,在文中并没有提到,可以麻烦介绍说明一下吗?
@BrianQy 对比了一下github的代码,在trie.go里多了一个方法travel,在router.go里多了一个getRouters的方法,在文中并没有提到,可以麻烦介绍说明一下吗?
travel是遍历某个root下的所有存在pattern的route,getRouters里面就是通过传入一个指定的Method(比如GET,POST),调用travel这个方法来获取该Method作为root下的所有route,返回的nodes就是一个个已注册的route,比如/p/:lang/hello 等
这节看得脑壳痛 T T
感觉这个前缀树的实现太简单粗暴了,每个Part都直接作为一个节点,这样子插入是方便了,但是每次匹配子节点效率很低,也有点浪费空间。
前缀树路由看着确实有点吃力,因为缺少注释且使用到了递归,稍微有点抽象,可能劝退一些人,所以贴上我的注释,希望对一些新手有所帮助。
package gee
import (
"fmt"
"strings"
)
type node struct {
pattern string // 是否一个完整的url,不是则为空字符串
part string // URL块值,用/分割的部分,比如/abc/123,abc和123就是2个part
children []*node // 该节点下的子节点
isWild bool // 是否模糊匹配,比如:filename或*filename这样的node就为true
}
func (n *node) String() string {
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}
// 找到匹配的子节点,场景是用在插入时使用,找到1个匹配的就立即返回
func (n *node) matchChild(part string) *node {
// 遍历n节点的所有子节点,看是否能找到匹配的子节点,将其返回
for _, child := range n.children {
// 如果有模糊匹配的也会成功匹配上
if child.part == part || child.isWild {
return child
}
}
return nil
}
// 一边匹配一边插入的方法
func (n *node) insert(pattern string, parts []string, height int) {
if len(parts) == height {
// 如果已经匹配完了,那么将pattern赋值给该node,表示它是一个完整的url
// 这是递归的终止条件
n.pattern = pattern
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil {
// 没有匹配上,那么进行生成,放到n节点的子列表中
child = &node{part: part, isWild: part[0] == ':' || part[0] == '*'}
n.children = append(n.children, child)
}
// 接着插入下一个part节点
child.insert(pattern, parts, height+1)
}
// 这个函数跟matchChild有点像,但它是返回所有匹配的子节点,原因是它的场景是用以查找
// 它必须返回所有可能的子节点来进行遍历查找
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
func (n *node) search(parts []string, height int) *node {
if len(parts) == height || strings.HasPrefix(n.part, "*") {
// 递归终止条件,找到末尾了或者通配符
if n.pattern == "" {
// pattern为空字符串表示它不是一个完整的url,匹配失败
return nil
}
return n
}
part := parts[height]
// 获取所有可能的子路径
children := n.matchChildren(part)
for _, child := range children {
// 对于每条路径接着用下一part去查找
result := child.search(parts, height+1)
if result != nil {
// 找到了即返回
return result
}
}
return nil
}
// 查找所有完整的url,保存到列表中
func (n *node) travel(list *([]*node)) {
if n.pattern != "" {
// 递归终止条件
*list = append(*list, n)
}
for _, child := range n.children {
// 一层一层的递归找pattern是非空的节点
child.travel(list)
}
}
跟着实现了一遍,有个疑问请教一下:既然最后都是根据method跟pattern作为key。。那好像跟我直接提取path作为key存储起来,然后匹配handle没啥区别?省去一大堆路由解析跟匹配
key := c.Method + "-" + n.pattern
r.handlers[key](c)
教一下:既然最后都是根据method跟pattern作为key。。那好像跟我直接提取path作为key存储起来,然后匹配handle没啥区别?省去一大堆路由解
因为需要模糊匹配
上面那个前缀树图中,最后右边两个节点应该是: /p/blog和 /p/related吧
接触Golang第三天, 单元测试那一段没有调用的地方啊
有点小瑕疵哈,对于/get/name/test,/get/:name同时存在的话这种情况没有考虑哈,gin框架下是直接报错了。我是这样修改了一下,insert方法:
func (n node)insert(pattern string,parts []string,height int){ if len(parts)==height{ n.pattern=pattern return } part:=parts[height] child:=n.matchChild(part) if child==nil{ child=&node{part: part,isWild: part[0]==':'||part[0]==''} if child.isWild &&len(n.children)>0{ panic(part+"同级已经有路由") } if part[0]=='' && len(parts)>height+1{ panic(part+"不能出现在中间") } if (part[0]==''||part[0]==':')&&len(part)==1{ panic(part+"不能单独出现") } n.children=append(n.children,child) } child.insert(pattern,parts,height+1) }
matchChild方法: func (n node)matchChild(part string)node{ for _,child:=range n.children{ if child.isWild{ panic(part+"同级已经有"+child.part) } if child.part==part{ return child } } return nil } 这样的话/get/name/test,/get/:name无论哪个在前就都会报错了
@LRQuan 接触Golang第三天, 单元测试那一段没有调用的地方啊
这个从零开始不是Go语言学习从零开始。。。
r.handlers[key] (c)
@huangnian130 r.handlers[key] (c)
这括号c是什么意思,有点不明白
r.handlers[key] 返回一个handerfunc,(c)是调用这个handlerfunc
https://geektutu.com/post/gee-day3.html
7天用 Go语言 从零实现Web框架教程(7 days implement golang web framework from scratch tutorial),用 Go语言/golang 动手写Web框架,从零实现一个Web框架,从零设计一个Web框架。本文介绍了如何用 Trie 前缀树实现路由。支持简单的参数解析和通配符的场景。