【Golang学习笔记】从零开始搭建一个Web框架(二)

文章目录

    • 模块化路由
    • 前缀树路由

前情提示:

【Golang学习笔记】从零开始搭建一个Web框架(一)-CSDN博客

模块化路由

路由在kilon.go文件中导致路由和引擎交织在一起,如果要实现路由功能的拓展增强,那将会非常麻烦,这无疑降低了代码的可读性和可维护性。现在的工作是将路由从引擎里剥离出来,引擎中仅对路由进行包装。

新建文件router.go,当前目录结构为:

myframe/├── kilon/│   ├── context.go│   ├── go.mod      [1]│   ├── kilon.go│   ├── router.go├── go.mod          [2]├── main.go

在router中添加下面内容:

package kilonimport ("net/http"
)type router struct {Handlers map[string]HandlerFunc
}
// 创建router对象
func newRouter() *router {return &router{make(map[string]HandlerFunc)}
}
// 剥离路由注册的具体实现
func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {key := method + "-" + patternr.Handlers[key] = handler
}
// 剥离SeverHTTP中路由处理的具体实现
func (r *router) handle(ctx *Context) {key := ctx.Method + "-" + ctx.Pathif handler, ok := r.Handlers[key]; ok {handler(ctx)} else {ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", ctx.Path)}
}

修改kilon.go文件:

package kilonimport ("net/http"
)type HandlerFunc func(*Context)type Origin struct {router *router // 修改路由
}func New() *Origin {return &Origin{router: newRouter()} // 修改构造函数
}func (origin *Origin) addRoute(method string, pattern string, handler HandlerFunc) {origin.router.addRoute(method, pattern, handler) // 修改调用
}func (origin *Origin) GET(pattern string, hander HandlerFunc) {origin.addRoute("GET", pattern, hander) 
}func (origin *Origin) POST(pattern string, hander HandlerFunc) {origin.addRoute("POST", pattern, hander) 
}func (origin *Origin) ServeHTTP(w http.ResponseWriter, req *http.Request) {ctx := newContext(w, req)origin.router.handle(ctx) // 调用router.go中的处理方法
}func (origin *Origin) Run(addr string) (err error) {return http.ListenAndServe(addr, origin)
}

至此,实现了路由的模块化,后续路由功能的增强将不会改动kilon.go文件。

前缀树路由

目前的路由表使用map存储键值对,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由而无法实现动态路由。在实际的应用中,可能需要使用正则表达式或者其他匹配规则来实现更复杂的路由匹配,而 map 无法提供这种功能。接下来,将使用前缀树(Tire树)实现动态路由,主要实现两个功能:

  • 参数匹配:。例如 /p/:name/doc,可以匹配 /p/zhangsan/doc/p/lisi/doc
  • 通配*(仅允许最后一个有"*"号)。例如 /static/*filepath,可以匹配/static/fav.ico/static/js/jQuery.js

新建文件trie.go,当前文件目录结构为:

myframe/├── kilon/│   ├── context.go│   ├── go.mod      [1]│   ├── kilon.go│   ├── router.go│   ├── tire.go├── go.mod          [2]├── main.go

在trie.go中创建前缀树的节点:

type node struct {patten   string  // 待匹配路由part     string  // 路由当前部分children []*node // 孩子节点isWild   bool    // 是否为模糊搜索,当含有":"和通配符"*"时为true
}

当注册路由"/p/:name/doc"、“/p/:name/png”、“/p/:lang/doc”、"/p/:lang/png"后,树中内容如下:

在这里插入图片描述

可以看到,pattern只有在插入最后一个子节点后才会设置,这是为了在查询路由信息时可以根据 pattern==""来判断改路由是否注册。isWaild的作用在于当part不匹配时,如果isWaild为true可以继续搜索,这样就实现了模糊匹配。

先实现路由注册时的前缀树插入逻辑:

func (n *node) insert(pattern string, parts[]string, index int)

pattern是注册路由地址,parts是解析pattern后的字符串数组(使用方法strings.Split(pattern, "/")进行解析)如"/p/:name/doc"对应 [“p”,“:name”,“doc”],parts[index]是当前需要插入的part。可以通过index判断是否退出。(疑问:如果只用Split解析那pattren="/"的时候不就无法注册了吗?答:开始时树的根节点的part为空,不会匹配,“p"一定会插入到根节点的子节点切片中。而当pattern为”/“时解析字符串切片为空,进入根节点的时候len(parts) = index = 0,会将根节点的pattern设置为”/“,也可以实现路由”/"的注册。)

代码如下:

func (n *node) insert(pattern string, parts[]string, index int){// 进来的时候说明 n.part = parts[index-1] 即最后一个 part 则直接设置 pattenif len(parts) == index {n.patten = patternreturn}// 还需匹配 part// 先在 n.children 切片中匹配 partpart := parts[index]child :=  n.matchChild(part)// 如果没有找到,则构建一个 child 并插入 n.children 切片中if child == nil {child = &node{part: part,// 含有":"或者通配符"*"时为 trueisWild: part[0] ==':' || part[0] == '*',}// 插入 n.children 切片n.children = append(n.children, child)}// 递归插入child.insert(pattern, parts, index + 1)
}
// 查找匹配 child
func (n *node) matchChild(part string) *node {// 遍历 n.children 查找 part 相同的 childfor _, child := range n.children {// 如果找到匹配返回 child, 当 isWild 为 true 时视为匹配实现模糊搜索if child.part == part || child.isWild == true {return child}}	// 没找到返回nilreturn nil
}

接下来实现接受请求时查询路由信息时的前缀树搜索逻辑:

func (n *node) search(parts []string, index int) *node

parts是路由地址的解析数组,index指向当前part索引

代码如下:

// 搜索
func (n *node) search(parts []string, index int) *node {// 如果匹配将节点返回if len(parts) == index || strings.HasPrefix(n.part, "*") {if n.pattern == "" {return nil}return n}part := parts[index]// 获取匹配的所有孩子节点nodes := n.matchChildren(part)// 递归搜索匹配的child节点for _, child := range nodes {result := child.search(parts, index+1)if result != nil {return result}}return nil
}
// 查找匹配的孩子节点,由于有":"和"*",所以可能会有多个匹配,因此返回一个节点切片
func (n *node) matchChildren(part string) []*node {nodes := make([]*node, 0)for _, child := range n.children {if child.part == part || child.isWild == true {nodes = append(nodes, child) // 将符合的孩子节点添入返回切片}}return nodes
}

至此trie.go暂时写完,现在在路由中进行应用,回到router.go文件。为了区分不同的方法如GET和POST,为每一个Method建立一颗前缀树,并以键值对的形式存储在一个map中:map[Method] = tire。修改router结构体与构造方法:

type router struct {roots     map[string]*node       // 前缀树mapHandlers map[string]HandlerFunc // 将pattern作为key获取/注册方法
}
func newRouter() *router {return &router{make(map[string]*node),make(map[string]HandlerFunc),}
}

将pattern插入前缀树之前,要先解析成字符串切片,现在需要实现一个解析函数。

func parsePattern(pattern string) []string {temp := strings.Split(pattern, "/")parts := make([]string, 0)for _, item := range temp {if item != ""{parts = append(parts, item)if item[0] == '*' {break}}	}return parts
}

修改注册路由的逻辑:

func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {parts := parsePattern(pattern) // 解析patternkey := method + "-" + patternif _, ok := r.roots[key]; !ok {r.roots[method] = &node{} // 如果没有则创建一个节点}r.roots[method].insert(pattern, parts, 0) // 前缀树插入patternr.Handlers[key] = handler			     // 注册方法
}

当接受请求时,需要对请求中携带的路由信息解析,并获取匹配的节点以及":“,”*"匹配到的参数,现在需要写一个路由获取方法:

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) // 解析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
}

路径中的参数应该交给上下文对象让用户便捷获取。在Context结构体中添加Params属性,并包装获取方法:

type Context struct {Writer     http.ResponseWriterReq        *http.RequestPath       stringMethod     stringParams     map[string]string // 路由参数属性StatusCode int
}
// 获取路径参数
func (c *Context) Param(key string) string {value := c.Params[key]return value
}

在router.go中的handle中应用路由获取方法,并将路径参数提交给上下文对象。

func (r *router) handle(ctx *Context) {n, params := r.getRoute(ctx.Method, ctx.Path) // 获取路由节点及参数字典ctx.Params = paramsif n != nil {key := ctx.Method + "-" + n.pattern // key为n的patternr.Handlers[key](ctx) // 调用注册函数} else {ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", ctx.Path)}
}

现在router.go内容为:

package kilonimport ("net/http""strings"
)type router struct {roots    map[string]*nodeHandlers map[string]HandlerFunc
}func newRouter() *router {return &router{make(map[string]*node),make(map[string]HandlerFunc),}
}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
}func (r *router) handle(ctx *Context) {n, params := r.getRoute(ctx.Method, ctx.Path)ctx.Params = paramsif n != nil {key := ctx.Method + "-" + n.patternr.Handlers[key](ctx)} else {ctx.String(http.StatusNotFound, "404 NOT FOUND: %s\n", ctx.Path)}
}func parsePattern(pattern string) []string {temp := strings.Split(pattern, "/")parts := make([]string, 0)for _, item := range temp {if item != "" {parts = append(parts, item)if item[0] == '*' {break}}}return parts
}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
}

在main.go测试一下:

package mainimport ("kilon""net/http"
)func main() {r := kilon.New()r.GET("/hello", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"message": "Hello World",})})r.GET("/hello/:username", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"message": ctx.Param("username"),})})r.GET("/hello/:username/*filename", func(ctx *kilon.Context) {ctx.JSON(http.StatusOK, kilon.H{"username": ctx.Param("username"),"filename": ctx.Param("filename"),})})r.Run(":8080")
}

分别访问下面地址,都可以看到响应信息

127.0.0.1:8080/hello

127.0.0.1:8080/hello/zhangsan

127.0.0.1:8080/hello/zhangsan/photo.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/815724.shtml

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

相关文章

基于Springboot+Vue的Java项目-课程作业管理系统(附演示视频+源码+LW)

大家好!我是程序员一帆,感谢您阅读本文,欢迎一键三连哦。 💞当前专栏:Java毕业设计 精彩专栏推荐👇🏻👇🏻👇🏻 🎀 Python毕业设计 &am…

rocketmq面试

broker主从复制机制 同步复制: 等Master和Slave均写成功后,才反馈给客户端写成功状态; 如果Master出故障, Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。…

使用LNMP部署动态网站环境

目录 实验环境 一、配置LNMP架构环境 二、验证部署的LNMP 动态网站环境是否可用 三、配置过程中遇到的问题及解决思路 实验环境 centos7 192.168.81.131/24 一、配置LNMP架构环境 概念及配置手册参考第20章 使用LNMP架构部署动态网站环境。 | 《Linux就该这么学》 安装g…

Java编程练习之接口的声明及实现

1.创建老师类和学生类,两个类都实现了问候接口和工作接口,模拟上课的场景,运行效果如下: package Zaria; interface hello{public void speak(); } interface work{public void dowork(); } class Student implements hello,work{…

gitee如何新建仓库并用小乌龟上传代码

目录 1.登录并注册gitee账号 2.创建新仓库 3.填写仓库信息 4.初始化本地仓库 5.上传数据 7.gitee官网查看上传文件 8.如何安装小乌龟 1.登录并注册gitee账号 2.创建新仓库 登录后,点击页面右上角的「」按钮,选择「新建仓库」。 3.填写仓库信息 …

入门Adaptive AUTOSAR(一) -- 为什么要提Adaptive(1)

目录 1.Adaptive AUTOSAR 1.1 AUTOSAR的由来 1.2 AUTOSAR的方法论 1.3 Why Adaptive 2.小结 1.Adaptive AUTOSAR 1.1 AUTOSAR的由来 2017年,国内绝大部分供应商还在思考如何用最小代价切入到AUTOSAR Classic Platform的时候,AUTOSAR Adaptive Pla…

把持中国互联网流量的“四大家族”,各个牛逼plus!

中国互联网80%流量被四大家族把持着,其余要么去这些家族批发流量,要么去抢占剩余20%。 以下是对中国互联网流量四大家族的介绍和代表性的流量入口产品: 百度系: 百度是中国最大的搜索引擎公司,其搜索引擎百度是中国互…

【位运算】Leetcode 两整数之和

题目解析 371. 两整数之和 算法讲解 异或的本质就是无进位相加,但是我们需要处理进位,就需要知道哪一位上有进位,再让无进位相加的结果 进位即可,在重复这个过程,当进位等于0的时候,说明相加的过程已经结…

OSPF防环文档

OPSF在区域内会产生俩类LSA:Router LSA ,Network LSA 路由器以自己为树根构建最短路径树 ,这里的最短路径树按两步形 成,第一步,仅考虑路由器和传输网络之间的连接。通过 Dijkstra 算法,根据链路状态数据…

吴恩达2022机器学习专项课程(一) 第二周课程实验:特征工程和多项式回归(Lab_04)

目标 探索特征工程和多项式回归,使用线性回归来拟合非常复杂甚至非线性的函数。 1.为什么线性回归能拟合非线性函数? fxw*xb,属于线性回归的扩展,这个公式在数学中不属于线性,因为有x,而在机器学习中属于…

图文教程 | 2024Typora最新版免费激活使用教程(新旧版可用)

一、打开官网下载最新版Typora Typora 官网下载 安装: Typora中文官网:https://typoraio.cn/ Typora官网:https://typora.io/releases/all 官网长这个样子 下面这个不是官网!!!!注意&#x…

通讯录的实现(顺序表版本)

我们知道通讯录是基于顺序表的前提下,要写好通讯录我们就要深入了解好顺序表。我们先来看看什么是顺序表。(注意今天代码量有点多,坚持一下)。冲啊!兄弟们! 顺序表的简单理解 对于顺序表,我们首…

用AI的视角看世界

前言 2024年将是Ai人工智能在各个行业垂直领域发展的元年。 随着2022年11月openai 的大语言模型chatgpt3.5的诞生,已经预示着互联网时代,移动互联网时代即将迎来新的变革,也预示着web3.0和元宇宙更近了一步。 回顾历史,互联网的…

使用hexo+gitee从零搭建个人博客

一、环境准备 1.Node.js:下载 | Node.js 中文网 (nodejs.cn) ,Hexo 是基于Node.js 的博客框架 教程:https://blog.csdn.net/weixin_52799373/article/details/123840137 node -v npm -v 安装 Node.js 淘宝镜像加速器 (cnpm&am…

大模型应用工具 LangChain 入门书籍: LangChain 简明讲义

书籍信息 书名:《LangChain 简明讲义:从 0 到 1 构建 LLM 应用程序》出版社:电子工业出版社书籍链接:https://item.jd.com/14105705.html书籍配套代码:https://github.com/kebijuelun/langchain_book 书籍背景 计算机…

力扣LeetCode138. 复制带随机指针的链表 两种解法(C语言实现)

目录 题目链接 题目分析 题目定位: 解题思路 解题思路1(粗暴但是复杂度高) 解题思路2(巧妙并且复杂度低) 题目链接 138. 复制带随机指针的链表https://leetcode-cn.com/problems/copy-list-with-random-pointer/ …

实战项目——智慧社区(四)之 系统管理

1、用户管理 提供查询和搜索用户、根据id查询用户信息、添加用户、修改用户、删除用户的功能 界面 添加用户 修改用户信息 2、角色管理 提供查询和搜索角色、根据id查询角色信息、添加角色、修改角色、删除角色的功能 界面 添加角色 修改角色 3、菜单管理 提供查询和搜索菜…

机器学习在安全领域的应用:从大数据中识别潜在安全威胁

🧑 作者简介:阿里巴巴嵌入式技术专家,深耕嵌入式人工智能领域,具备多年的嵌入式硬件产品研发管理经验。 📒 博客介绍:分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向的学习指导…

蓝桥杯第2152题——红绿灯

问题描述 爱丽丝要开车去上班, 上班的路上有许多红绿灯, 这让爱丽丝很难过。为 了上班不迟到, 她给自己的车安装了氮气喷射装置。现在她想知道自己上班最 短需要多少时间。 爱丽丝的车最高速度是 米每秒, 并且经过改装后, 可以瞬间加速到小于 等于最高速的任意速度, 也可以瞵…

Sonar下启动发生错误,elasticsearch启动错误

Download | SonarQube | Sonar (sonarsource.com) 1.首先我的sonar版本为 10.4.1 ,java版本为17 2.sonar启动需要数据库,我先安装了mysql, 但是目前sonar从7.9开始不支持mysql,且java版本要最少11,推荐使用java17 3.安装postsql,创建sonar数据库 4.启…