手把手和你一起实现一个Web框架实战——EzWeb框架(三)[Go语言笔记]Go项目实战

代码仓库:

github

gitee

中文注释,非常详尽,可以配合食用

本篇代码,请选择demo3

这一篇文章我们进行动态路由解析功能的设计,

如xxx/:id/xxx,xxx/xxx/*mrxuexi.md

实现这处理这两类模式的简单小功能,实现起来不简单,原有的map[path]HandlerFunc数据结构只能存储静态路由与方法对应,而无法处理动态路由,我们使用一种树结构来进行路由表的存储。

一、设计这个数据结构

1、节点结构体设计

type node struct {
path string /* 需要匹配的整体路由 */
part string /* 路由中的一部分,例如 :lang */
children []*node /* 存储子节点们 */
isBlurry bool /* 如果模糊匹配则为true */
}

2、一个传入part后,通过遍历该节点的全部子节点们,找到拥有相同part的子节点的方法(返回首个)

func (n *node) matchChild(part string) *node {
//遍历子节点们,对比子节点的part和part是否相同,是或者遍历到的子节点支持模糊匹配则返回该子节点
for _, child := range n.children {
if child.part == part || child.isBlurry {
return child
}
}
return nil
}

3、一个返回匹配的子节点们的方法(返回全部,包括动态路由的存储的部分)

func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
//遍历选择满足条件的子节点,加入到nodes中,然后返回
for _, child := range n.children {
if child.part == part || child.isBlurry {
nodes = append(nodes, child)
}
}
return nodes
}

4、构造路由表的插入方法,parts[]存储的是根据路由path分解出来的part们,我们拿到part则取检索子节点是否存在这个part,不存在则新建一个子节点,不停的在这个树上深入,直到遍历完我们的全部part,然后递归返回。

//插入方法,用一个递归实现,找匹配的路径直到找不到匹配当前part的节点,新建
func (n *node) insert(path string, parts []string, height int) {
//如果遍历到底部了,则将我们的path存入节点,开始返回。递归的归来条件。
if len(parts) == height{
n.path = path
return
}
//获取这一节的part,并进行搜索
part := parts[height]
child := n.matchChild(part) //若没有搜索到匹配的子节点,则根据目前的part构造一个子节点
if child == nil {
child = &node{
part: part,
isBlurry: part[0] == ':' || part[0] == '*',
}
n.children = append(n.children, child)
}
child.insert(path, parts, height+1)
}

5、我们带着part们一个个在存储路由表的树中查找,我们拿到某个节点的全部子节点,找到满足part相同或者isBlurry:true的节点。通过递归再往深处挖,挖下去直到发现某一级节点的子节点们,没有对应匹配的part,又返回来,再去上一层的子节点看,这就是一个深度优先遍历的情况。

//搜索方法
func (n *node) search(parts []string, height int) *node {
//如果节点到头,或者存在*前缀的节点,开始返回
if len(parts) == height || strings.HasPrefix(n.part,"*") {
//如果此时遍历到的n没有存储对应的path,说明未到目标最底层,则返回空
if n.path == "" {
return nil
}
return n
}
//搜索找到满足part的子节点们放入children
part := parts[height]
children := n.matchChildren(part)
//接着遍历子节点们,递归调用获得下一级的子节点们,要走到头的同时,找到了对应的节点,才返回最终我们找到的result
//这里为什么要遍历子节点们进行深入搜索,因为它还存在满足isBlurry:true的节点,我们也需要在其中深入搜索。
for _, child := range children {
result := child.search(parts, height+1)
if result != nil {
//返回满足要求的节点
return result
}
}
return nil
}

二、更新路由表的存储结构和处理方法

1、其中roots中的第一层是roots[method]*node

type router struct {
//用于存储相关方法
handlers map[string]HandlerFunc
//用于存储每种请求方式的树的根节点
roots map[string]*node
}

2、设计一个parsePath方法,对外部传入的路由根据"/"进行分割,存入parts

// parsePath 用于处理传入的url,先将其分开存储到parts中,当然出现*前缀的部分就可以结束
func parsePath(path string) []string {
vs := strings.Split(path, "/")
parts := make([]string, 0)
for _, v := range vs {
if v != "" {
parts = append(parts, v)
if v[0] == '*' {
break
}
}
}
return parts
}

3、routeraddRoute 方法,在 handlers map[string]HandlerFunc 中存入路由对应处理方法,进行路由注册。存入形式为例如:{ "GET-/index" : 定义的处理方法 }

注意这里的path使我们用来构造路由表要存入的目标path

// router 中 addRoute 方法,在 handlers map[string]HandlerFunc 中存入路由对应处理方法
//存入形式为例如:{ "GET-/index" : 定义的处理方法 }
func (r *router) addRoute(method string, path string, handler HandlerFunc) {
parts := parsePath(path)
log.Printf("Route %4s - %s",method,path)
key := method + "-" + path
_, ok := r.roots[method]
//roots中不存在对应的方法入口则注册相应方法入口
if !ok {
r.roots[method] = &node{}
}
//调用路由表插入方法,在该数据结构中插入该路由
r.roots[method].insert(path, parts, 0)
//把method-path作为key,以及handler方法作为value注入数据结构
r.handlers[key] = handler
}

4、做一个getRoute方法,进入到对应路由树,找到我们的路由,通过哈希表存入处理动态路由拿到param和找到的*node一起返回。

注意代码中的n.path是我们注册在路由表中的路由,path是外部传入的!

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
searchParts := parsePath(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 := parsePath(n.path) //n.path包含了完整的路由 for i, part := range parts {//遍历这一条路径
//拿到:的参数,存入params,方法中的part作为key,外面传入的path中的数据作为value存入
if part[0] == ':' {
params[part[1:]] = searchParts[i]
}
//拿到*,此时路由表中的存入的part作为key,外面传入的path中的数据作为value传入params,之后也再没有了
if part[0] == '*' && len(part) > 1{
params[part[1:]] = strings.Join(searchParts[i:],"/")
break
}
}
return n, params
}
return nil, nil
}

5.同时我们的hanle方法和上一篇文章不同的是,不是直接拿外部传入的path直接在 handlers map[string]HandlerFunc找对应的方法,因为我们外部传入的path是动态的。我们是先通过getRoute方法拿到参数和对应的找到存储节点,用这个节点中存储的path(它是静态的,是我们之前注入的),再在 handlers map[string]HandlerFunc找到对应的方法。

//根据context中存储的 c.Method 和 c.Path 拿到对应的处理方法,进行执行,如果拿到的路由没有注册,则返回404
func (r *router) handle(c *Context) {
//获取匹配到的节点,同时也拿到两类动态路由中参数
n, params := r.getRoute(c.Method, c.Path)
if n != nil {
c.Params = params
//拿目的节点中的path做key来找handlers
key := c.Method + "-" + n.path
r.handlers[key](c)
}else {
c.String(http.StatusNotFound,"404 NOT FOUND")
}
}

三、Context变更

1、修改Context结构体,构造Params来存放处理动态路由拿到的参数

// Context 结构体,内部封装了 http.ResponseWriter, *http.Request
type Context struct {
Writer http.ResponseWriter
Req *http.Request
//请求的信息,包括路由和方法
Path string
Method string
Params map[string]string /*用于存储外面拿到的参数 ":xxx" or "*xxx" */
//响应的状态码
StatusCode int
}

2、设计Param方法,拿到处理动态路由的获取参数

// Param 是c的Param的value的获取方法
func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}

随便做个测试:

/*
@Time : 2021/8/16 下午4:01
@Author : mrxuexi
@File : main
@Software: GoLand
*/
package main import (
"Ez"
"net/http"
)
func main() {
r := Ez.New() r.POST("/hello/:id/*filepath", func(c *Ez.Context) {
c.JSON(http.StatusOK,Ez.H{
"name" : c.PostForm("name"),
"age" : c.PostForm("age"),
"id" : c.Param("id"),
"filepath" : c.Param("filepath"),
})
}) r.Run(":9090")
}

成功!

参考:

[1]: https://github.com/geektutu/7days-golang/tree/master/gee-web ""gee""

[2]: https://github.com/gin-gonic/gin ""gin""

手把手和你一起实现一个Web框架实战——EzWeb框架(三)[Go语言笔记]Go项目实战的更多相关文章

  1. 手把手和你一起实现一个Web框架实战——EzWeb框架(二)[Go语言笔记]Go项目实战

    手把手和你一起实现一个Web框架实战--EzWeb框架(二)[Go语言笔记]Go项目实战 代码仓库: github gitee 中文注释,非常详尽,可以配合食用 上一篇文章我们实现了框架的雏形,基本地 ...

  2. 手把手和你一起实现一个Web框架实战——EzWeb框架(四)[Go语言笔记]Go项目实战

    手把手和你一起实现一个Web框架实战--EzWeb框架(四)[Go语言笔记]Go项目实战 代码仓库: github gitee 中文注释,非常详尽,可以配合食用 这一篇文章主要实现路由组功能.实现路由 ...

  3. 手把手和你一起实现一个Web框架实战——EzWeb框架(五)[Go语言笔记]Go项目实战

    手把手和你一起实现一个Web框架实战--EzWeb框架(五)[Go语言笔记]Go项目实战 代码仓库: github gitee 中文注释,非常详尽,可以配合食用 本篇代码,请选择demo5 中间件实现 ...

  4. Go语言笔记[实现一个Web框架实战]——EzWeb框架(一)

    Go语言笔记[实现一个Web框架实战]--EzWeb框架(一) 一.Golang中的net/http标准库如何处理一个请求 func main() { http.HandleFunc("/& ...

  5. 潭州课堂25班:Ph201805201 WEB 之 页面编写 第三课 (课堂笔记)

    index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset=&quo ...

  6. JAVA Web day03--- Android小白的第三天学习笔记

    3.5.6.Math对象(了解) 无需创建,直接Math.方法来进行使用.(内置对象) Math方法 random() 随机生成0~1数字 round(x) 对X进行四舍五入 3.5.7.RegExp ...

  7. Python之路,Day18 - 开发一个WEB聊天来撩妹吧

    Python之路,Day18 - 开发一个WEB聊天来撩妹吧   本节内容: 项目实战:开发一个WEB聊天室 功能需求: 用户可以与好友一对一聊天 可以搜索.添加某人为好友 用户可以搜索和添加群 每个 ...

  8. 【转载】ASP.NET MVC Web API 学习笔记---第一个Web API程序

    1. Web API简单说明 近来很多大型的平台都公开了Web API.比如百度地图 Web API,做过地图相关的人都熟悉.公开服务这种方式可以使它易于与各种各样的设备和客户端平台集成功能,以及通过 ...

  9. ASP.NET MVC Web API 学习笔记---第一个Web API程序

    http://www.cnblogs.com/qingyuan/archive/2012/10/12/2720824.html GetListAll /api/Contact GetListBySex ...

随机推荐

  1. 获取 Windows 密码「GitHub 热点速览 v.21.28」

    作者:HelloGitHub-小鱼干 安全问题一直是 GitHub 的一大热点,因为数据安全问题诞生的各类自托管服务便是.而本周周榜上的 2 个和安全主题相关的项目,有些不同.mimikatz 是个老 ...

  2. Nginx:Nginx日志切割方法

    Nginx的日志文件是没有切割(rotate)功能的,但是我们可以写一个脚本来自动切割日志文件. 首先我们要注意两点: 1.切割的日志文件是不重名的,所以需要我们自定义名称,一般就是时间日期做文件名. ...

  3. Java中的四种引用和引用队列

    目录 强引用 软引用 弱引用 幻象引用 Reachability Fence 参考 强引用 正常的引用,生命周期最长,例如 Object obj = new Object(); 当JVM内存不足时,宁 ...

  4. 「CF986F」 Oppa Funcan Style Remastered

    「CF986F」 Oppa Funcan Style Remastered Link 首先发现分解成若干个 \(k\) 的因数很蠢,事实上每个因数都是由某个质因子的若干倍组成的,所以可以将问题转换为分 ...

  5. C语言:键盘输入

    C语言有多个函数可以从键盘获得用户输入,它们分别是: scanf():和 printf() 类似,scanf() 可以输入多种类型的数据. getchar().getche().getch():这三个 ...

  6. [刘阳Java]_MyBatis_其他方式来实现多表查询的操作_第9讲

    MyBatis其他方式来实现多表查询的操作 利用Java中的集合框架(List,Map) 其中List存储多个查询返回的记录 Map查询返回字段,同时记录表中一条数据 <?xml version ...

  7. 【剑指offer】52. 两个链表的第一个公共节点

    剑指 Offer 52. 两个链表的第一个公共节点 知识点:链表: 题目描述 输入两个链表,找出它们的第一个公共节点. 如下面的两个链表: 示例 示例1: 输入:intersectVal = 8, l ...

  8. PAT甲级:1036 Boys vs Girls (25分)

    PAT甲级:1036 Boys vs Girls (25分) 题干 This time you are asked to tell the difference between the lowest ...

  9. 解决 Github 打不开或打开很慢的问题

    解决 Github 打不开或打开很慢的问题 方法一 一.确定 github 网站的 ip 打开网址:http://github.com.ipaddress.com/ 192.30.253.112 gi ...

  10. 高德纳/Donald Ervin Knuth

    丸了丸了这位就是我人生的第一位爱豆了owo 感觉他的经历,气质都是我期望的类型呀. 即使没有人家的智商和绝顶天赋,也不断向彼努力吧. 从小喜欢音乐,会多种乐器(管风琴) 其实长得人高马大,高中校篮球队 ...