本篇是Mygin的第四篇
目的
- 使用 Trie 树实现动态路由解析。
- 参数绑定
前缀树
本篇比前几篇要复杂一点,原来的路由是用map实现,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。遇到类似hello/:name这动态路由就无能为力了,实现动态路由最常用的数据结构,被称为前缀树。这种结构非常适用于路由匹配。比如我们定义了如下路由:
- /a/b/c
- /a/b
- /a/c
- /a/b/c/d
- /a/:name/c
- /a/:name/c/d
- /a/b/:name/e
在前缀树中的结构体
HTTP请求的路径是由/分隔的字符串构成的,所以用/拆分URL字符串,得到不同的树节点,且有对应的层级关系。
- Mygin/tree.go 首先看tree中node结构定义
type node struct {
children []*node //子节点
part string //树节点
wildChild bool //是否是精确匹配
handlers HandlersChain //路由回调,实际的请求
nType nodeType //节点类型 默认static params
fullPath string //完整路径
}
- Mygin/tree.go 具体实现
package mygin
import (
"strings"
)
type nodeType uint8
// 路由的类型
const (
static nodeType = iota
root
param
catchAll
)
// 不同的method 对应不同的节点树 定义
type methodTree struct {
method string
root *node
}
// Param 参数的类型key=> value
type Param struct {
Key string
Value string
}
// Params 切片
type Params []Param
type methodTrees []methodTree
type node struct {
children []*node
part string
wildChild bool
handlers HandlersChain
nType nodeType
fullPath string
}
// Get 获取 参数中的值
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
}
// ByName 通过ByName获取参数中的值 会忽略掉错误,默认返回 空字符串
func (ps Params) ByName(name string) (v string) {
v, _ = ps.Get(name)
return
}
// 根据method获取root
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
}
// 添加路径时
func (n *node) addRoute(path string, handlers HandlersChain) {
//根据请求路径按照'/'划分
parts := n.parseFullPath(path)
//将节点插入路由后,返回最后一个节点
matchNode := n.insert(parts)
//最后的节点,绑定执行链
matchNode.handlers = handlers
//最后的节点,绑定完全的URL,后续param时有用
matchNode.fullPath = path
}
// 按照 "/" 拆分字符串
func (n *node) parseFullPath(fullPath string) []string {
splits := strings.Split(fullPath, "/")
parts := make([]string, 0)
for _, part := range splits {
if part != "" {
parts = append(parts, part)
if part == "*" {
break
}
}
}
return parts
}
// 根据路径 生成节点树
func (n *node) insert(parts []string) *node {
part := parts[0]
//默认的字节类型为静态类型
nt := static
//根据前缀判断节点类型
switch part[0] {
case ':':
nt = param
case '*':
nt = catchAll
}
//插入的节点查找
var matchNode *node
for _, childNode := range n.children {
if childNode.part == part {
matchNode = childNode
}
}
//如果即将插入的节点没有找到,则新建一个
if matchNode == nil {
matchNode = &node{
part: part,
wildChild: part[0] == '*' || part[0] == ':',
nType: nt,
}
//新子节点追加到当前的子节点中
n.children = append(n.children, matchNode)
}
//当最后插入的节点时,类型赋值,且返回最后的节点
if len(parts) == 1 {
matchNode.nType = nt
return matchNode
}
//匹配下一部分
parts = parts[1:]
//子节点继续插入剩余字部分
return matchNode.insert(parts)
}
// 根据路由 查询符合条件的节点
func (n *node) search(parts []string, searchNode *[]*node) {
part := parts[0] //a
allChild := n.matchChild(part) //b c :name
if len(parts) == 1 {
// 如果到达路径末尾,将所有匹配的节点加入结果
*searchNode = append(*searchNode, allChild...)
return
}
parts = parts[1:] //b
for _, n2 := range allChild {
// 递归查找下一部分
n2.search(parts, searchNode)
}
}
// 根据part 返回匹配成功的子节点
func (n *node) matchChild(part string) []*node {
allChild := make([]*node, 0)
for _, child := range n.children {
if child.wildChild || child.part == part {
allChild = append(allChild, child)
}
}
return allChild
}
上诉路由中,实现了插入insert和匹配search时的功能,插入时安装拆分后的子节点,递归查找每一层的节点,如果没有匹配到当前part的节点,则新建一个。查询功能,同样也是递归查询每一层的节点。
- Mygin/router.go
package mygin
import (
"net/http"
)
type Router struct {
trees methodTrees
}
// 添加路由方法
func (r *Router) addRoute(method, path string, handlers HandlersChain) {
//根据method获取root
rootTree := r.trees.get(method)
//如果root为空
if rootTree == nil {
//初始化一个root
rootTree = &node{part: "/", nType: root}
//将初始化后的root 加入tree树中
r.trees = append(r.trees, methodTree{method: method, root: rootTree})
}
rootTree.addRoute(path, handlers)
}
// Get Get方法
func (r *Router) Get(path string, handlers ...HandlerFunc) {
r.addRoute(http.MethodGet, path, handlers)
}
// Post Post方法
func (e *Engine) Post(path string, handlers ...HandlerFunc) {
e.addRoute(http.MethodPost, path, handlers)
}
-
router中修改不大
-
Mygin/engine.go
package mygin
import (
"net/http"
)
// HandlerFunc 定义处理函数类型
type HandlerFunc func(*Context)
// HandlersChain 定义处理函数链类型
type HandlersChain []HandlerFunc
// Engine 定义引擎结构,包含路由器
type Engine struct {
Router
}
// ServeHTTP 实现http.Handler接口的方法,用于处理HTTP请求
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 获取对应HTTP方法的路由树的根节点
root := e.trees.get(r.Method)
// 解析请求路径
parts := root.parseFullPath(r.URL.Path)
// 查找符合条件的节点
searchNode := make([]*node, 0)
root.search(parts, &searchNode)
// 没有匹配到路由
if len(searchNode) == 0 {
w.Write([]byte("404 Not found!\n"))
return
}
// 参数赋值
params := make([]Param, 0)
searchPath := root.parseFullPath(searchNode[0].fullPath)
for i, sp := range searchPath {
if sp[0] == ':' {
params = append(params, Param{
Key: sp[1:],
Value: parts[i],
})
}
}
// 获取处理函数链
handlers := searchNode[0].handlers
if handlers == nil {
w.Write([]byte("404 Not found!\n"))
return
}
// 执行处理函数链
for _, handler := range handlers {
handler(&Context{
Request: r,
Writer: w,
Params: params,
})
}
}
// Default 返回一个默认的引擎实例
func Default() *Engine {
return &Engine{
Router: Router{
trees: make(methodTrees, 0, 9),
},
}
}
// Run 启动HTTP服务器的方法
func (e *Engine) Run(addr string) error {
return http.ListenAndServe(addr, e)
}
package main
import (
"gophp/mygin"
)
func main() {
// 创建一个默认的 mygin 实例
r := mygin.Default()
// 定义路由处理函数
handleABC := func(context *mygin.Context) {
context.JSON(map[string]interface{}{
"path": context.Request.URL.Path,
})
}
// 注册路由
r.Get("/a/b/c", handleABC)
r.Get("/a/b", handleABC)
r.Get("/a/c", handleABC)
// 注册带参数的路由
r.Get("/a/:name/c", func(context *mygin.Context) {
name := context.Params.ByName("name")
path := "/a/" + name + "/c"
context.JSON(map[string]interface{}{
"path": path,
})
})
r.Get("/a/:name/c/d", func(context *mygin.Context) {
name := context.Params.ByName("name")
path := "/a/" + name + "/c/d"
context.JSON(map[string]interface{}{
"path": path,
})
})
r.Get("/a/b/:name/e", func(context *mygin.Context) {
name := context.Params.ByName("name")
path := "/a/b" + name + "/e"
context.JSON(map[string]interface{}{
"path": path,
})
})
r.Get("/a/b/c/d", handleABC)
// 启动服务器并监听端口
r.Run(":8088")
}
标签:string,parts,Mygin,part,func,动态,节点,路由,name
From: https://www.cnblogs.com/pengb/p/17980370