从零实现gin day03 动态前缀树路由

101 阅读3分钟

前言

前面的章节我们使用map的数据结构实现了从路由的匹配.然后web框架的动态路由匹配的问题是map结构不能够解决的.比如/hello/:name 可以匹配/hello/ziyu /hello/wangwu 等路由.所以本章的目标是使用前缀树构建动态路由匹配

  • 前缀树知识
  • 前缀树与框架的融合

前缀树

代码实现前缀树

node.go

package main  
  
import (  
    "strings"  
)  
  
type node struct {  
    patten string //完整的路由 /hello/:name  
    part string //一部分路径 /hello  
    child []*node //包含的子节点  
    isWild bool //是否是模糊匹配 : || * 的时候为true  
}  
  
//匹配到一个part的时候就返回这个节点  
func (n *node) matchChild(part string) *node {  
    for _, child := range n.child {  
        if child.part == part || n.isWild {  
            return n  
        }  
    }  
    return nil  
}  
  
//匹配所有符合这个part的节点,并且返回  
func (n *node) matchChildren(part string) []*node {  
    nodes := make([]*node, 0)  
    for _, child := range n.child {  
        if child.part == part || child.isWild {  
            nodes = append(nodes, child)  
        }  
    }  
    return nodes  
}  
  
//按照 ‘/’拆分字符串  
func parsePatten(patten string) []string {  
    parts := make([]string, 0)  
    split := strings.Split(patten, "/")  
    for _, part := range split {  
        if part != "" {  
            parts = append(parts, part)  
            if part[0] == '*' {  
                break  
            }  
        }  
    }  
    return parts  
}  
  
//根据前缀树往node插入parts  
func (n *node) insert(patten string, parts []string, index int) {  
    if len(parts) == index {  
        n.patten = patten  
        return  
    }  
    part := parts[index]  
    child := n.matchChild(part)  
    if child == nil {  
            child = &node{  
            part: part,  
            isWild: part[0] == '*' || part[0] == ':',  
        }  
        n.child = append(n.child, child)  
    }  
    child.insert(patten, parts, index+1)  
}  
  
//搜索匹配这个parts的node 直到最后一个节点  
func (n *node) search(parts []string, index int) *node {  
    if len(parts) == index || strings.HasPrefix(n.part, "*") {  
        return n  
    }  
    part := parts[index]  
    children := n.matchChildren(part)  
    for _, child := range children {  
        search := child.search(parts, index+1)  
        if search != nil {  
            return search  
        }  
    }  
    return nil  
}

比较复杂难懂的还是前缀树这里.首页我们定了一个node结构.其中最主要的就是insert方法.在insert方法中,我们去递归匹配part,如果part不存在则创建一个新的节点,并将其插入到node中.例子:GET /hello/:name首先调用AddRouter去创建一个r.roots[GET] = &node{}.然后insert的时候,第一个part为/hello我们插入第一次发现没有就创建一个节点A挂在node下面,之后在A.insert;匹配/:name发现也没有,就挂在A下面.

改造之后的router.go

package main  
  
import (  
    "net/http"  
    "strings"  
)  
  
type Router struct {  
    roots map[string]*node  
    handlers map[string]HandleFunc  
}  
//构造函数  
func newRouter() *Router {  
    return &Router{  
        roots: make(map[string]*node),  
        handlers: make(map[string]HandleFunc),  
    }  
}  
//构建前缀树  
func (r *Router) AddRouter(method string, path string, handle HandleFunc) {  
    key := method + "-" + path  
    if _, ok := r.roots[method]; !ok {  
        r.roots[method] = &node{}  
    }  
    parts := parsePatten(path)  
    r.roots[method].insert(path, parts, 0)  
    r.handlers[key] = handle  
}  
//根据路由获取node和参数  
func (r *Router) GetRouter(method string, path string) (*node, map[string]string) {  
        parts := parsePatten(path)  
        n := r.roots[method]  
        search := n.search(parts, 0)  
        params := make(map[string]string, 0)  
        if search != nil {  
            patten := parsePatten(search.patten)  
        for index, part := range patten {  
                if part[0] == ':' {  
                    params[part[1:]] = parts[index]  
                }  
                if part[0] == '*' && len(part) > 1 {  
                    params[part[1:]] = strings.Join(parts[index:], "/")  
                    break  
                }  
            }  
            return search, params  
        }  
  
    return nil, nil  
}  
func (r *Router) Post(path string, handle HandleFunc) {  
    r.AddRouter("POST", path, handle)  
}  
func (r *Router) Get(path string, handle HandleFunc) {  
    r.AddRouter("GET", path, handle)  
}  
func (r *Router) Handle(c *Context) {  
    n, params := r.GetRouter(c.Method, c.Path)  
    if n != nil {  
        c.Params = params  
        //这里要注意因为是动态匹配要用n.patten不能用c.Path 因为Path是变化的  
        key := c.Method + "-" + n.patten  
        h := r.handlers[key]  
        h(c)  
    } else {  
        c.String(http.StatusNotFound, "404 page not find %s\n", c.Path)  
    }  

}

main.go

package main  
  
import (  
    "net/http"  
)  
  
func main() {  
  
    e := newEngine()  
    e.router.Get("/", func(ctx *Context) {  
        ctx.HTML(200, "index 首页")  
    })  
    e.router.Get("/hello/:name", func(ctx *Context) {  
        ctx.String(http.StatusOK, "hello %s,you're at %s\n", ctx.Param("name"), ctx.Path)  
    })  
    e.router.Get("/assert/*filename", func(ctx *Context) {  
        ctx.JSON(http.StatusOK, H{  
            "filepath": ctx.Param("filename"),  
        })  
    })  

    e.Run(":7999")  
}