学习Go语言Web框架Gee总结--前缀树路由Router(三)

news/2024/11/22 22:28:36/

学习Go语言Web框架Gee总结--前缀树路由Router

  • router/gee/trie.go
  • router/gee/router.go
  • router/gee/context.go
  • router/main.go

学习网站来源:Gee

项目目录结构:
在这里插入图片描述

router/gee/trie.go

实现动态路由最常用的数据结构,被称为前缀树(Trie树)

关于前缀树:

  1. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  2. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  3. 每个节点的所有子节点包含的字符都不相同
package geeimport ("fmt""strings"
)//node结构体表示路由节点,包含了待匹配的路由pattern、路由中的一部分part、子节点children和是否精确匹配isWild
type node struct {pattern  string // 待匹配路由,例如 /p/:langpart     string // 路由中的一部分,例如 :langchildren []*node // 子节点,例如 [doc, tutorial, intro]isWild   bool // 是否精确匹配,part 含有 : 或 * 时为true
}/* 
用于将节点的信息格式化为字符串的方法
该方法返回一个包含节点模式(pattern)、节点部分(part)和是否是通配符(isWild)的字符串
其中%t是用于格式化布尔值的占位符
例如,如果有一个节点n,它的pattern为"/user/:id",part为":id",isWild为true,那么调用n.String()会返回以下格式的字符串:
"node{pattern=/user/:id, part=:id, isWild=true}"
*/
func (n *node) String() string {return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.isWild)
}/*
用于遍历路由树中的节点,并将具有路由模式的节点添加到传入的列表中
方法接收一个指向节点切片的指针作为参数,然后遍历当前节点及其子节点,将具有路由模式的节点添加到传入的节点切片中
首先,如果当前节点的pattern不为空(即具有路由模式),则将当前节点添加到传入的节点切片中。
然后,遍历当前节点的子节点,对每个子节点递归调用travel方法,将子节点及其子节点的路由模式节点添加到传入的节点切片中
*/
func (n *node) travel(list *[]*node) {if n.pattern != "" {*list = append(*list, n)}for _, child := range n.children {child.travel(list)}
}/*
用于在子节点中找到第一个匹配成功的节点,如果子节点的part和传入的part相等,或者子节点是通配符(isWild为true)
则返回该子节点。如果没有匹配成功的子节点,则返回nil
*/
func (n *node) matchChild(part string) *node {for _, child := range n.children {if child.part == part || child.isWild {return child}}return nil
}/*
用于在子节点中找到所有匹配成功的节点,如果子节点的part和传入的part相等,或者子节点是通配符(isWild为true)
则将该子节点加入到nodes切片中。最后返回nodes切片,其中包含了所有匹配成功的子节点
*/
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
}/*
用于向路由树中插入路由。它接收三个参数:pattern表示要插入的路由模式,parts表示路由模式分割后的部分,height表示当前插入的层级
首先,它检查当前层级是否已经是最后一层(即parts的长度是否等于height),如果是,则将当前节点的pattern设置为传入的pattern,表示找到了对应的路由
否则,它从parts中取出当前层级的部分,然后调用matchChild方法查找是否已经存在对应的子节点
如果没有找到对应的子节点,说明需要创建一个新的子节点,然后将其插入到当前节点的children中
然后,递归调用insert方法,将pattern、parts和height+1传入新创建的子节点中,继续插入下一层级的路由
*/
func (n *node) insert(pattern string, parts []string, height int) {if len(parts) == height {n.pattern = patternreturn}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)
}/*
用于在路由树中搜索路由。它接收两个参数:parts表示路由模式分割后的部分,height表示当前搜索的层级
首先,它检查当前层级是否已经是最后一层(即parts的长度是否等于height),或者当前节点是一个通配符节点(part以*开头)
如果是,则检查当前节点是否有对应的路由模式,如果有,则返回当前节点,表示找到了对应的路由
否则,它从parts中取出当前层级的部分,然后调用matchChildren方法查找所有匹配的子节点
然后,对于每一个匹配的子节点,递归调用search方法,将parts和height+1传入子节点中,继续搜索下一层级的路由
如果在递归过程中找到了对应的路由节点,则直接返回该节点;如果没有找到,则返回nil
*/
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}}return nil
}

router/gee/router.go

Trie 树的插入与查找都成功实现了,接下来我们将 Trie 树应用到路由中去吧。我们使用 roots 来存储每种请求方式的Trie 树根节点。使用 handlers 存储每种请求方式的 HandlerFunc 。getRoute 函数中,还解析了:和*两种匹配符的参数,返回一个 map

package geeimport ("net/http""strings"
)//用于存储路由树和处理函数
type router struct {roots    map[string]*nodehandlers map[string]HandlerFunc
}func newRouter() *router {return &router{roots:    make(map[string]*node),handlers: make(map[string]HandlerFunc),}
}//用于解析路由模式的函数
func parsePattern(pattern string) []string {vs := strings.Split(pattern, "/")parts := make([]string, 0)for _, item := range vs {if item != "" {parts = append(parts, item)if item[0] == '*' {break}}}return parts
}/*
用于向路由器中添加路由
首先调用 parsePattern 函数解析路由模式,然后将方法和模式拼接成键,并检查 roots 中是否存在对应的方法
如果不存在,则将方法添加到 roots 中,然后调用 insert 方法将模式插入到路由树中,并将处理函数添加到 handlers 中
*/
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {parts := parsePattern(pattern)key := method + "-" + pattern_, ok := r.roots[method]if !ok {r.roots[method] = &node{}}r.roots[method].insert(pattern, parts, 0)r.handlers[key] = handler
}/*
用于根据请求方法和路径获取路由
首先调用 parsePattern 函数解析路径,然后检查 roots 中是否存在对应的方法
如果存在,则调用 search 方法在路由树中查找匹配的节点,并将匹配的部分和参数存储到 params 中
最后返回匹配的节点和参数
*/
func (r *router) getRoute(method string, path string) (*node, map[string]string) {searchParts := parsePattern(path)params := make(map[string]string)root, ok := r.roots[method]if !ok {return nil, nil}n := root.search(searchParts, 0)if n != nil {parts := parsePattern(n.pattern)for index, part := range parts {if part[0] == ':' {params[part[1:]] = searchParts[index]}if part[0] == '*' && len(part) > 1 {params[part[1:]] = strings.Join(searchParts[index:], "/")break}}return n, params}return nil, nil
}/*
用于获取指定请求方法下的所有路由节点
首先检查 roots 中是否存在对应的方法
如果存在,则调用 travel 方法遍历路由树并将节点存储到切片中,最后返回该切片
*/
func (r *router) getRoutes(method string) []*node {root, ok := r.roots[method]if !ok {return nil}nodes := make([]*node, 0)root.travel(&nodes)return nodes
}/*
用于处理请求
首先调用 getRoute 方法获取匹配的路由节点和参数
然后根据匹配的节点和请求方法拼接键,从 handlers 中取出对应的处理函数并调用它
如果找不到匹配的路由节点,则返回 404 NOT FOUND
*/
func (r *router) handle(c *Context) {n, params := r.getRoute(c.Method, c.Path)if n != nil {c.Params = paramskey := c.Method + "-" + n.patternr.handlers[key](c)} else {c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)}
}

router/gee/context.go

由于需要能够访问到解析的参数,需要对context对象增加关于param的属性与方法

type Context struct {// origin objectsWriter http.ResponseWriterReq    *http.Request// request infoPath   stringMethod stringParams map[string]string// response infoStatusCode int
}func (c *Context) Param(key string) string {value, _ := c.Params[key]return value
}

router/main.go

package main
import ("net/http""gee"
)func main() {r := gee.New()r.GET("/", func(c *gee.Context) {c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")})r.GET("/hello", func(c *gee.Context) {c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)})r.GET("/hello/:name", func(c *gee.Context) {c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)})//添加了一个带有通配符的路由//当用户访问 /assets/some/file/path 路径时,会执行传入的匿名函数,该函数从路径参数中获取 filepath 的值//并将其包含在 JSON 响应中返回r.GET("/assets/*filepath", func(c *gee.Context) {c.JSON(http.StatusOK, gee.H{"filepath": c.Param("filepath")})})r.Run(":9999")
}

这样,动态路由就成功实现了


http://www.ppmy.cn/news/1290631.html

相关文章

【MySQL】CRUD,常见函数及unionunion

目录 一 CRUD 查询 新增数据 修改数据 删除数据 二 常见函数 ① 字符函数 ② 数字函数 ③ 日期函数 ④ 流程控制函数 ⑤ 聚合函数 三 union&union 含义 语法 一 CRUD 条件查询执行顺序&#xff1a;where(条件) group by(分组) having(筛选) order by(排序)…

57个Linux常用命令含参数介绍和使用示例

点击下载《57个Linux常用命令含参数介绍和使用示例》 1. pwd 作用&#xff1a;显示当前所在的工作目录的全路径名称 //显示当前目录 pwd该命令无需任何参数&#xff0c;只需在终端窗口中输入 pwd 命令即可使用。 2. cd 作用&#xff1a;更改当前工作目录。 //跳转目录至D…

基于SSM的校园快递管理系统

目录 前言 开发环境以及工具 项目功能介绍 学生&#xff1a; 管理员&#xff1a; 详细设计 获取源码 前言 本项目是一个基于IDEA和Java语言开发的基于SSM的校园快递管理系统应用。应用包含学生端和管理员端等多个功能模块。 欢迎使用我们的校园快递管理系统&#xff01;我…

前端发开的性能优化 请求级:请求前(资源预加载和预读取)

预加载 预加载&#xff1a;是优化网页性能的重要技术&#xff0c;其目的就是在页面加载过程中先提前请求和获取相关的资源信息&#xff0c;减少用户的等待时间&#xff0c;提高用户的体验性。预加载的操作可以尝试去解决一些类似于减少首次内容渲染的时间&#xff0c;提升关键资…

开源协议简介和选择

软件国产化已经提到日程上了&#xff0c;先来研究一下开源协议。 引言 在追求“自由”的开源软件领域的同时不能忽视程序员的权益。为了激发程序员的创造力&#xff0c;现今世界上有超过60种的开源许可协议被开源促进组织&#xff08;Open Source Initiative&#xff09;所认可…

算法训练营第三十天|332.重新安排行程 51. N皇后 37. 解数独

目录 Leetcode332.重新安排行程Leetcode51. N皇后Leetcode37. 解数独 Leetcode332.重新安排行程 文章链接&#xff1a;代码随想录 题目链接&#xff1a;332.重新安排行程 class Solution { public:unordered_map<string, map<string, int>> targets;bool backtrack…

【linux 多线程并发】多任务调度器,调度策略时间片轮转,先进先出,多种实时任务的策略,内核级最高优先级调度策略

任务调度器 ​专栏内容&#xff1a; 参天引擎内核架构 本专栏一起来聊聊参天引擎内核架构&#xff0c;以及如何实现多机的数据库节点的多读多写&#xff0c;与传统主备&#xff0c;MPP的区别&#xff0c;技术难点的分析&#xff0c;数据元数据同步&#xff0c;多主节点的情况下…

2024.1.2 Redis 数据类型 Stream、Geospatial、HyperLogLog、Bitmaps、Bitfields 简介

目录 引言 Stream 类型 Geospatial 类型 HyperLogLog 类型 Bitmaps 类型 Bitfields 类型 引言 Redis 最关键&#xff08;应用广泛、频繁使用&#xff09;的五个数据类型 StringListHashSetZSet 下文介绍的数据类型一般适合在特定的场景中使用&#xff01; Stream 类型 St…