前言
前面的章节我们使用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")
}