04 中间件-提高框架的可拓展性

news2024/9/23 18:20:23

到目前为止我们已经完成了 Web 框架的基础部分,使用 net/http 启动了一个 Web 服务,并且定义了自己的 Context,可以控制请求超时。

在前面的controller.go中有一个超时控制逻辑:


func FooControllerHandler(c *framework.Context) error {
  ...
    // 在业务逻辑处理前,创建有定时器功能的 context
  durationCtx, cancel := context.WithTimeout(c.BaseContext(), time.Duration(1*time.Second))
  defer cancel()

  go func() {
    ...
    // 执行具体的业务逻辑
        
    time.Sleep(10 * time.Second)
        // ...
              
    finish <- struct{}{}
  }()
  // 在业务逻辑处理后,操作输出逻辑...
    select {
  ...
  case <-finish:
    fmt.Println("finish")
  ...
  }
  return nil
}

这部分由业务逻辑- time.Sleep 函数所代表的逻辑 和非业务逻辑- 比如创建 Context、通道等待 finish 信号等构成, 为了复用代码,我们将非业务逻辑抽象处理,封装好,对外提供使用,这就是中间件。

代码的组织顺序很清晰,先预处理请求,再处理业务逻辑,最后处理返回值,很符合设计模式中的装饰器模式。装饰器模式,顾名思义,就是在核心处理模块的外层增加一个又一个的装饰,类似洋葱。

在这里插入图片描述
现在,抽象出中间件的思路是不是就很清晰了,把核心业务逻辑先封装起来,然后一层一层添加装饰,最终让所有请求正序一层层通过装饰器,进入核心处理模块,再反序退出装饰器。原理就是这么简单,不难理解,我们接着看该如何实现。

使用函数嵌套方式实现中间件

装饰器模式是一层一层的,所以具体实现其实也不难想到,就是使用函数嵌套。

首先,我们封装核心的业务逻辑。就是说,这个中间件的输入是一个核心的业务逻辑 ControllerHandler,输出也应该是一个 ControllerHandler。所以对于一个超时控制器,我们可以定义一个中间件为 TimeoutHandler。


func TimeoutHandler(fun ControllerHandler, d time.Duration) ControllerHandler {
  // 使用函数回调
  return func(c *Context) error {

    finish := make(chan struct{}, 1)
    panicChan := make(chan interface{}, 1)

    // 执行业务逻辑前预操作:初始化超时 context
    durationCtx, cancel := context.WithTimeout(c.BaseContext(), d)
    defer cancel()

    c.request.WithContext(durationCtx)

    go func() {
      defer func() {
        if p := recover(); p != nil {
          panicChan <- p
        }
      }()
      // 执行具体的业务逻辑
      fun(c)

      finish <- struct{}{}
    }()
    // 执行业务逻辑后操作
    select {
    case p := <-panicChan:
      log.Println(p)
      c.responseWriter.WriteHeader(500)
    case <-finish:
      fmt.Println("finish")
    case <-durationCtx.Done():
      c.SetHasTimeout()
      c.responseWriter.Write([]byte("time out"))
    }
    return nil
  }
}

中间件函数的返回值是一个匿名函数,这个匿名函数实现了 ControllerHandler 函数结构,参数为 Context,返回值为 error。

则注册路由就可以这样写了:

// 在核心业务逻辑 UserLoginController 之外,封装一层 TimeoutHandler
core.Get("/user/login", framework.TimeoutHandler(UserLoginController, time.Second))

这种函数嵌套方式,让下层中间件是上层中间件的参数,通过一层层嵌套实现了中间件的装饰器模式。
但是有两个问题:

  1. 中间件是循环嵌套的,当有多个中间件的时候,整个嵌套长度就会非常长,非常不优雅的
TimeoutHandler(LogHandler(recoveryHandler(UserLoginController)))
  1. 刚才的实现,只能为单个业务控制器设置中间件,不能批量设置

使用 pipeline 思想改造中间件

一层层嵌套不好用,如果我们将每个核心控制器所需要的中间件,使用一个数组链接(Chain)起来,形成一条流水线(Pipeline),就能完美解决这两个问题了。

这个 Pipeline 模型和前面的洋葱模型不一样的点在于,Middleware 不再以下一层的 ControllerHandler 为参数了,它只需要返回有自身中间件逻辑的 ControllerHandler。


// 超时控制器参数中ControllerHandler结构已经去掉
func Timeout(d time.Duration) framework.ControllerHandler {
  // 使用函数回调
  return func(c *framework.Context) error {
      //...
    }
}

我们可以将每个中间件构造出来的 ControllerHandler 和最终的业务逻辑的 ControllerHandler 结合在一起,成为一个 ControllerHandler 数组,也就是控制器链。在最终执行业务代码的时候,能一个个调用控制器链路上的控制器。

这个想法其实是非常自然的,因为中间件中创造出来的 ControllerHandler 匿名函数,和最终的控制器业务逻辑 ControllerHandler,都是同样的结构,所以我们可以选用 Controllerhander 的数组,来表示某个路由的业务逻辑。

第一步,我们需要修改路由节点 node。
在 node 节点中将原先的 Handler,替换为控制器链路 Handlers。

第二步,我们修改 Context 结构。
在中间件注册的回调函数中,只有 framework.Context 这个数据结构作为参数,所以在 Context 中也需要保存这个控制器链路 (handlers),并且要记录下当前执行到了哪个控制器(index)。

第三步,来实现链条调用方式。
为了控制实现链条的逐步调用,我们为 Context 实现一个 Next 方法。这个 Next 方法每调用一次,就将这个控制器链路的调用控制器,往后移动一步。继续在框架文件夹中的 context.go 文件里写:

// 核心函数,调用context的下一个函数
func (ctx *Context) Next() error {
  ctx.index++
  if ctx.index < len(ctx.handlers) {
    if err := ctx.handlers[ctx.index](ctx); err != nil {
      return err
    }
  }
  return nil
}

Next() 函数会在框架的两个地方被调用:

  • 第一个是在此次请求处理的入口处,即 Core 的 ServeHttp;
  • 第二个是在每个中间件的逻辑代码中,用于调用下个中间件。

如何注册控制器链路

  • Core 和 Group 单独设计一个 Use 函数,为其数据结构负责的路由批量设置中间件
  • 为 Core 和 Group 注册单个路由的 Get / Post / Put / Delete 函数,设置中间件

// core中使用use注册中间件
core.Use(
    middleware.Test1(),
    middleware.Test2())

// group中使用use注册中间件
subjectApi := core.Group("/subject")
subjectApi.Use(middleware.Test3())


// 注册路由规则
func registerRouter(core *framework.Core) {
  // 在core中使用middleware.Test3() 为单个路由增加中间件
  core.Get("/user/login", middleware.Test3(), UserLoginController)

  // 批量通用前缀
  subjectApi := core.Group("/subject")
  {
        ...
        // 在group中使用middleware.Test3() 为单个路由增加中间件
    subjectApi.Get("/:id", middleware.Test3(), SubjectGetController)
  }
}

基本的中间件: Recovery

// recovery机制,将协程中的函数异常进行捕获
func Recovery() framework.ControllerHandler {
  // 使用函数回调
  return func(c *framework.Context) error {
    // 核心在增加这个recover机制,捕获c.Next()出现的panic
    defer func() {
      if err := recover(); err != nil {
        c.Json(500, err)
      }
    }()
    // 使用next执行具体的业务逻辑
    c.Next()

    return nil
  }
}

【小结】

  1. 中间件函数返回的也是ControlHandler,因此将中间件和路由处理函数作成一个链表,一起挂在node节点上
  2. 查找到路由函数后,之前是一个函数直接调用即可,现在是一个链表,要按顺序逐个调用,因为函数都是接收ctx参数,返回err,因此要将此链表也存放到ctx中,并且再存一个idx字段表示执行到哪一步骤。具体的执行需要定义一个ctx.Next()方法,每个中间件内部要调用此方法,才能实现链式调用
  3. 注册路由中间件时,有批量Use方式和单个Get、Post、Put、Delete方式,最终的目的都是将所有中间件和处理函数作为链表挂到node节点上,以待后代找到链表并逐个调用。

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

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

相关文章

【Ap AutoSAR入门与实战开发04】:服务的需求定义以及如何在arxml中定义服务

总目录链接==>> AutoSAR入门和实战系列总目录 文章目录 1 服务的需求定义2 服务的arxml定义2.1 事件中的数据类型定义我们在「【Ap AutoSAR入门与实战开发03】-【Ap_s2s模块02】:到底什么是基于信号,什么是基于服务,两者的主要区别是什么?」的文章中讲到了讲到了服…

如何在ONLYOFFICE v7.3中使用 WRAPROWS、WRAPCOLS公式

在ONLYOFFICE7.3版本更新以来&#xff0c;每次给大家都分享几种函数公式的运用方式&#xff0c;今天在给大家分享两种&#xff0c;分别是&#xff1b;WRAPROWS、WRAPCOLS。 ONLYOFFICE ONLYOFFICE文档是一款免费开源在线办公软件&#xff0c;可以打开阅读并编辑文档、表格和幻…

1247. 交换字符使得字符串相同

1247. 交换字符使得字符串相同 难度中等162收藏分享切换为英文接收动态反馈 有两个长度相同的字符串 s1 和 s2&#xff0c;且它们其中 只含有 字符 "x" 和 "y"&#xff0c;你需要通过「交换字符」的方式使这两个字符串相同。 每次「交换字符」的时候&am…

轻松入门H3C无线AC上线AP【入门篇】

我们知道华三的最新模拟器支持了无线AC的配置&#xff0c;今天就浅浅的出个无线AC的教程&#xff0c;你上也会的那种。今天我们模拟的是二层环境下&#xff0c;笔者准备了2个AP&#xff0c;以此展示AP上线到AC的教程&#xff0c;并且用手机测试WiFi连接正常&#xff0c;且客户端…

7、nodejs安装

前言&#xff1a;工具下载地址阿里云盘&#xff1a;nodejs&#xff1a;https://www.aliyundrive.com/s/hLAKBgjNUqr提取码: p9q9一、介绍Node.js发布于2009年5月&#xff0c;由Ryan Dahl开发&#xff0c;是一个基于Chrome V8引擎的JavaScript运行环境&#xff0c;使用了一个事件…

【VUE】二 vue指令

目录 一、插值表达式 二、v-bind指令(对标签中的属性进行操作) 三、v-model指令&#xff08;input、select、textarea等。【双向绑定】&#xff09; 四、v-for循环指令 五、v-on(事件指令) 六、v-if条件判断 七、v-show&#xff08;条件显示或隐藏&#xff09; 八、案例…

模型解释性:PFI、PDP、ICE等包的用法

本篇主要介绍几种其他较常用的模型解释性方法。 1. Permutation Feature Importance(PFI) 1.1 算法原理 置换特征重要性(Permutation Feature Importance)的概念很简单&#xff0c;其衡量特征重要性的方法如下&#xff1a;计算特征改变后模型预测误差的增加。如果打乱该特征的…

HCIP-5距离矢量路由协议RIP学习笔记

前言 路由信息协议RIP&#xff08;Routing Information Protocol&#xff09;的简称&#xff0c;它是一种基于距离矢量&#xff08;Distance-Vector&#xff09;算法的协议&#xff0c;使用跳数作为度量来衡量到达目的网络的距离。RIP主要应用于规模较小的网络中。Rip是第一个动…

如何创建“杀手级”SaaS 产品文档?

SaaS 产品的文档至关重要&#xff0c;尽管在 SaaS 初创公司的旋风中&#xff0c;它可能在您的列表中并不重要。它不仅仅是为客户支持节省成本。您可能已经在发布一些文档时做了一些尝试&#xff0c;但现在是时候将您的文档提升到一个新的水平了。由于 SaaS 公司采用订阅模式运营…

【Git】Git使用(保姆级讲解)

1、第一次安装使用 git下载地址&#xff1a;https://git-scm.com/download 1.1 配置用户名、邮箱 ​ 这是非常重要的&#xff0c;因为每次Git提交都会使用该用户信息。 设置 ​ 在 git bash 输入以下命令。 git config --global user.name "" git config --glo…

CV学习笔记-ResNet

ResNet 文章目录ResNet1. ResNet概述1.1 常见卷积神经网络1.2 ResNet提出背景2. ResNet网络结构2.1 Residual net2.2 残差神经单元2.3 Shortcut2.4 ResNet50网络结构3. 代码实现3.1 Identity Block3.2 Conv Block3.3 ResNet网络定义3.4 整体代码测试1. ResNet概述 1.1 常见卷积…

【三维几何学习】MeshCNN: A Network with an Edge

MeshCNN引言一、方法简述1.1 输入1.2 卷积1.3 池化二、实验分析三、改进以及应用引言 MeshCNN是第一个将网格简化引入到池化操作中的网络&#xff1a;合并顶点降低网格分辨率&#xff0c;类似图像中的平均池化。 主页1:https://ranahanocka.github.io/MeshCNN/ 比较详细的讲解…

Java 练习题:输出纯素数

文章目录纯素数简介任务要求思路解析源码奉上运行效果总结纯素数简介 所谓纯素数就是该数本身不仅是素数&#xff0c;并且该数的每一位都是素数。 例如&#xff1a;23,37是纯素数&#xff0c;但13,29不是。 任务要求 输出55555内所有的纯素数&#xff0c;按每行20个的格式化…

JVM 学习(2)—简单理解Java 四大引用(强、软、弱、虚)

一、Java 引用概述 Java 中出现四种引用是为了更加灵活地管理对象的生命周期&#xff0c;以便在不同场景下灵活地处理对象的回收问题。不同类型的引用在垃圾回收时的处理方式不同&#xff0c;可以用来实现不同的垃圾回收策略。Java 目前将其分成四类&#xff0c;类图如下&…

彻底搞懂inner join,left join,right join

1.inner join A inner join B where 条件&#xff0c; 对于A表中的每一行都会去B表的所有行去查找&#xff0c;去匹配&#xff0c;符合条件的就将这两行连接起来 下面用一个例子来帮助实际理解这句话&#xff1a; 创建下面两个表&#xff0c;customers表orders表 输入以下s…

银行软件测试面试题目总结,希望可以帮到你

目录 一、根据题目要求写出具体LINUX操作命令 二、JMETER题目 三、根据题目要求写出具体SQL语句 总结感谢每一个认真阅读我文章的人&#xff01;&#xff01;&#xff01; 重点&#xff1a;配套学习资料和视频教学 一、根据题目要求写出具体LINUX操作命令 1、分别写出一种…

HTML标签——列表标签 之 自定义列表

HTML标签——列表标签 之 自定义列表 目录HTML标签——列表标签 之 自定义列表一、 场景&#xff1a;在网页的底部导航中通常会使用自定义列表实现。二、标签组成&#xff1a;三、案例实操四、运行效果五、显示特点&#xff1a;六、注意点&#xff1a;七、小结一、 场景&#x…

DeepPath: A Reinforcement Learning Method forKnowledge Graph Reasoning

Innovation使用RL学习KG中的关系路径推理使用Supervised Policy Learning解决&#xff1a;KG中关系图大&#xff0c;如试错训练RL&#xff0c;难以收敛使用双向路径搜索&#xff0c;减少中间节点数量IntroductionPRA是一种学习推理路径的方法&#xff0c;使用基于RandomWalk的重…

智能语音信息处理团队18篇论文被语音技术顶会ICASSP 2023接收

近日&#xff0c;ICASSP 2023会议发出了审稿结果通知&#xff0c;语音及语言信息处理国家工程研究中心智能语音信息处理团队共18篇论文被会议接收&#xff0c;论文方向涵盖语音识别、语音合成、话者识别、语音增强、情感识别、声音事件检测等&#xff0c;各接收论文简介见后文。…

FreeRTOS入门(05):事件组

文章目录目的基础说明相关函数使用演示总结目的 事件组是RTOS中相对常用的用于任务间交互的功能&#xff0c;这篇文章将对相关内容做个介绍。 本文代码测试环境见前面的文章&#xff1a;《FreeRTOS入门&#xff08;01&#xff09;&#xff1a;基础说明与使用演示》 基础说明…