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

代码仓库:

github

gitee

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

本篇代码,请选择demo3

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

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

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

一、设计这个数据结构

1、节点结构体设计

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. func (r *router) getRoute(method string, path string) (*node, map[string]string) {
  2. searchParts := parsePath(path)
  3. params := make(map[string]string)
  4. root, ok := r.roots[method]
  5. if !ok {
  6. return nil, nil
  7. }
  8. n := root.search(searchParts, 0)//传入全部路径的字符串数组,寻找到最后对应节点
  9. if n != nil {
  10. parts := parsePath(n.path) //n.path包含了完整的路由
  11. for i, part := range parts {//遍历这一条路径
  12. //拿到:的参数,存入params,方法中的part作为key,外面传入的path中的数据作为value存入
  13. if part[0] == ':' {
  14. params[part[1:]] = searchParts[i]
  15. }
  16. //拿到*,此时路由表中的存入的part作为key,外面传入的path中的数据作为value传入params,之后也再没有了
  17. if part[0] == '*' && len(part) > 1{
  18. params[part[1:]] = strings.Join(searchParts[i:],"/")
  19. break
  20. }
  21. }
  22. return n, params
  23. }
  24. return nil, nil
  25. }

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

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

三、Context变更

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

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

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

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

随便做个测试:

  1. /*
  2. @Time : 2021/8/16 下午4:01
  3. @Author : mrxuexi
  4. @File : main
  5. @Software: GoLand
  6. */
  7. package main
  8. import (
  9. "Ez"
  10. "net/http"
  11. )
  12. func main() {
  13. r := Ez.New()
  14. r.POST("/hello/:id/*filepath", func(c *Ez.Context) {
  15. c.JSON(http.StatusOK,Ez.H{
  16. "name" : c.PostForm("name"),
  17. "age" : c.PostForm("age"),
  18. "id" : c.Param("id"),
  19. "filepath" : c.Param("filepath"),
  20. })
  21. })
  22. r.Run(":9090")
  23. }

成功!

参考:

[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. buu [MRCTF2020]keyboard

    密文: ooo yyy ii w uuu ee uuuu yyy uuuu y w uuu i i rr w i i rr rrr uuuu rrr uuuu t ii uuuu i w u rrr ...

  2. Leetcode No.167 Two Sum II - Input array is sorted(c++实现)

    1. 题目 1.1 英文题目 Given an array of integers numbers that is already sorted in non-decreasing order, fi ...

  3. C# 8.0和.NET Core 3.0高级编程 分享笔记二:编程基础第一部分

    基础部分被我分为了2篇,因为实在太多了,但是每一个知识点我都不舍得删除,所以越写越多,这一篇博客整理了4个夜晚,内容有点多建议慢慢看.本章涵盖以下主题: 介绍C# 理解C#的基础知识 使用变量 处理空 ...

  4. RabbitMQ入门教程 [转]

    1.引言 RabbitMQ--Rabbit Message Queue的简写,但不能仅仅理解其为消息队列,消息代理更合适.消息队列主要解决应用耦合,异步消息,流量削锋等问题.实现高性能,高可用,可伸缩 ...

  5. Python获取list中指定元素的索引

    在平时开发过程中,经常遇到需要在数据中获取特定的元素的信息,如到达目的地最近的车站,橱窗里面最贵的物品等等.怎么办?看下面 方法一: 利用数组自身的特性 list.index(target), 其中a ...

  6. 入门Kubernetes-minikube本地k8s环境

    前言: 在上一篇 结尾中使用到了minikube方式来做k8s本地环境来学习k8s. 那么这篇先了解下minikube及使用 一.Minikube 简介 minikube 在 macOS.Linux ...

  7. 前端-HTML基础+CSS基础

    .pg-header { height: 48px; text-align: center; line-height: 48px; background-color: rgba(127, 255, 2 ...

  8. SpringBoot缓存管理(三) 自定义Redis缓存序列化机制

    前言 在上一篇文章中,我们完成了SpringBoot整合Redis进行数据缓存管理的工作,但缓存管理的实体类数据使用的是JDK序列化方式(如下图所示),不便于使用可视化管理工具进行查看和管理. 接下来 ...

  9. C语言:模拟密码输入显示星号

    一个安全的程序在用户输入密码时不应该显示密码本身,而应该回显星号或者点号,例如······或******,这在网页.PC软件.ATM机.POS机上经常看到.但是C语言没有提供类似的功能,控制台上只能原 ...

  10. vue实现menu菜单懒加载

    本文将在vue+element ui项目中简单实现menu菜单的懒加载. 最近接到这样的需求:菜单的选项不要固定的,而是下一级菜单选项需要根据上级菜单调接口来获取.what? 这不就是懒加载吗?翻了一 ...