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

news2024/10/5 13:43:14

学习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 gee

import (
	"fmt"
	"strings"
)

//node结构体表示路由节点,包含了待匹配的路由pattern、路由中的一部分part、子节点children和是否精确匹配isWild
type node struct {
	pattern  string // 待匹配路由,例如 /p/:lang
	part     string // 路由中的一部分,例如 :lang
	children []*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 = 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)
}

/*
用于在路由树中搜索路由。它接收两个参数: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 gee

import (
	"net/http"
	"strings"
)

//用于存储路由树和处理函数
type router struct {
	roots    map[string]*node
	handlers 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 = params
		key := c.Method + "-" + n.pattern
		r.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 objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	Params map[string]string
	// response info
	StatusCode 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.coloradmin.cn/o/1352709.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用Audio2Face驱动UE - MetaHuman

新的一年咯&#xff0c;很久没发博客了&#xff0c;就发两篇最近的研究吧。 开始之前说句话&#xff0c;别轻易保存任何内容&#xff0c;尤其是程序员不要轻易Ctrl S 在UE中配置Audio2Face 先检查自身电脑配置看是否满足&#xff0c;按最小配置再带个UE可能会随时崩&#x…

类的生命周期/加载

理一下&#xff0c;java 编译后的字节码文件&#xff0c;我们已经熟悉了 字节码文件长什么样&#xff0c;字节码文件中有哪些内容&#xff0c;那么下一步就是使用类加载器 把字节码文件加载到JVM中 类的生命周期 类的生命周期是 JVM类加载的基础。 加载 所谓加载&#xff0c;…

贝叶斯推断:细谈贝叶斯变分和贝叶斯网络

1. 贝叶斯推断 统计推断这件事大家并不陌生&#xff0c;如果有一些采样数据&#xff0c;我们就可以去建立模型&#xff0c;建立模型之后&#xff0c;我们通过对这个模型的分析会得到一些结论&#xff0c;不管我们得到的结论是什么样的结论&#xff0c;我们都可以称之为是某种推…

【深度学习下载大型数据集】快速下载谷歌云盘数据集

个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 跑深度学习的时候,一些数据集比较大,比如60多个G,而且只是训练集. 然后这些数据是由某些实验室组采集的,并不像一些大公司搞的,一般都直接方法一些网盘中. 如果是谷歌网盘,本身通过代理也不麻烦,但是发现即使通过代…

为什么大学c语言课不顺便教一下Linux,Makefile

为什么大学c语言课不顺便教一下Linux&#xff0c;Makefile&#xff0c;git&#xff0c;gdb等配套工具链呢? 在开始前我有一些资料&#xff0c;是我根据自己从业十年经验&#xff0c;熬夜搞了几个通宵&#xff0c;精心整理了一份「Linux的资料从专业入门到高级教程工具包」&…

【2024.01.03】转行小白-刷css面试题01

总结 1.margin 负值问题 margin-top 和 margin-left 负值&#xff0c;元素向上、向左移动&#xff0c;自己动margin-right 负值&#xff0c;右侧元素左移&#xff0c;自身不受影响&#xff0c;别人动margin-bottom 负值&#xff0c;下方元素上移&#xff0c;自身不受影响 &am…

第十四章 :案例课:部暑KVM虚拟化平台

[rootLinux01 ~]# mount /dev/cdrom /mnt //挂载安装KVM需要的软件 [rootLinux01 ~]# yum -y install qemu-kvm-tools [rootLinux01 ~]# yum -y install qemu-kvm [rootLinux01 ~]# yum -y install virt-install [rootLinux01 ~]# yum -y install qemu-img [rootLinux01 ~]#…

求一个整数二进制中1的个数(三种方法详解)

越过寒冬 前言 今天复习了一些操作符的知识&#xff0c;看到了这道题&#xff0c;并且发先有三种解题思路&#xff0c;觉得有趣&#xff0c;据记下来与诸位分享一下。 题目 写一个函数&#xff0c;给定一个整数&#xff0c;求他的二进制位中1的个数 思路1 既然是二进制位那…

JOSEF约瑟 断电延时继电器 SRTD-220VDC-2H2D 导轨安装

系列型号&#xff1a; SRTD-24VDC-1H1D断电延时继电器&#xff1b;SRTD-110VDC-1H1D断电延时继电器&#xff1b; SRTD-220VDC-1H1D断电延时继电器&#xff1b;SRTD-110VAC-1H1D断电延时继电器&#xff1b; SRTD-220VAC-1H1D断电延时继电器&#xff1b;SRTD-24VDC-2H断电延时继电…

一文搞懂手机卡的定向流量到底是什么!

最近有一些小伙伴对于手机卡流量中包含的定向流量这个概念不是很明白&#xff0c;而且也不知道具体如何使用&#xff0c;今天这个视频&#xff0c;葫芦弟就仔细给大家讲解一下&#xff0c;希望能解开小伙伴们心中的疑惑。废话不多说&#xff0c;我们直接进入正题&#xff01; 首…

网络安全—模拟ARP欺骗

文章目录 网络拓扑安装使用编辑数据包客户机攻击机验证 仅做实验用途&#xff0c;禁止做违法犯罪的事情&#xff0c;后果自负。当然现在的计算机多无法被欺骗了&#xff0c;开了防火墙ARP欺骗根本无效。 网络拓扑 均使用Windows Server 2003系统 相关配置可以点击观看这篇文章…

git rebase(变基)应用场景

文章目录 git rebase(变基)应用场景1.git rebase -i HEAD~3 git rebase(变基)应用场景 使得提交记录变得简洁 现在我们模拟我们有多次提交记录&#xff0c;本地仓库有三条提交 整合成一条提交记录 1.git rebase -i HEAD~3 提交记录合并 HEAD~3合并三条记录 执行之后 然后把…

【Python机器学习】构建简单的k近邻算法模型

k近邻算法是一个很容易理解的算法&#xff0c;构建模型只需要保存训练数据集。要对一个新的数据点做出预测&#xff0c;算法会在训练集中寻找与这个新数据点距离最近的数据点&#xff0c;然后将找到的数据点的标签赋值给这个新数据点。 l近邻算法中k的含义是&#xff1a;我们可…

Ubuntu 常用命令之 locate 命令用法介绍

&#x1f525;Linux/Ubuntu 常用命令归类整理 locate命令是在Ubuntu系统下用于查找文件或目录的命令。它使用一个预先构建的数据库&#xff08;通常由updatedb命令创建&#xff09;来查找文件或目录&#xff0c;因此它的查找速度非常快。 plocate 安装 locate 不是 Ubuntu 系…

jdk动态代理中invoke的return返回的值有什么用?

目录 首先在接口中定义一个行为再定义一个目标角色实现接口&#xff0c;实现行为去代理角色类中解决一下报错&#xff0c;但是什么都不要写 invoke的return返回的值是调用方法中返回的值 下面我们来实例看一下 首先在接口中定义一个行为 public String toMarry02();再定义一个…

【论文阅读|冷冻电镜】DISCA: High-throughput cryo-ET structural pattern mining

论文题目 High-throughput cryo-ET structural pattern mining by unsupervised deep iterative subtomogram clustering 摘要 现有的结构排序算法的吞吐量低&#xff0c;或者由于依赖于可用模板和手动标签而固有地受到限制。本文提出了一种高吞吐量的、无需模板和标签的深度…

开源大模型应用开发

1.大语言模型初探 ChatGLM3简介 ChatGLM3-6B 是一个基于 Transformer 的预训练语言模型&#xff0c;由清华大学 KEG 实验室和智谱 AI 公司于 2023 年共同训练发布。该模型的基本原理是将大量无标签文本数据进行预训练&#xff0c;然后将其用于各种下游任务&#xff0c;例如文…

c++基础(对c的扩展)

文章目录 命令空间引用基本本质引用作为参数引用的使用场景 内联函数引出基本概念 函数补充默认参数函数重载c中函数重载定义条件函数重载的原理 命令空间 定义 namespace是单独的作用域 两者不会相互干涉 namespace 名字 { //变量 函数 等等 }eg namespace nameA {int num;v…

力扣每日一题99:恢复二叉搜索树

题目 给你二叉搜索树的根节点 root &#xff0c;该树中的 恰好 两个节点的值被错误地交换。请在不改变其结构的情况下&#xff0c;恢复这棵树 。 示例 1&#xff1a; 输入&#xff1a;root [1,3,null,null,2] 输出&#xff1a;[3,1,null,null,2] 解释&#xff1a;3 不能是 1 …

数脉观察二丨 详解CroPoolv2.0锁仓收益机制 文末附锁仓教程

1月1日元旦佳节期间&#xff0c;CyberVein基金会支持打造的CroPoolv2.0最新版本正式上线&#xff0c;获得了圈内媒体和知名KOL多方的关注&#xff0c;在Staking领域掀起了热议&#xff0c;用户可以前往CroPool.net进行锁仓体验。 CroPool v2.0新增“锁仓”功能板块&#xff0c…