【Gee】Day5:中间件

server/2025/2/24 10:58:16/

Day5:中间件

今天的任务是:

  • 设计并实现 Web 框架的中间件(Middlewares)机制。
  • 实现通用的 Logger 中间件,能够记录请求到响应所花费的时间,代码约 50 行。
    请添加图片描述

中间件是什么

中间件简单来说就是非业务的技术类组件。Web 框架本身不可能理解所有的业务,因而不可能实现所有的功能。因此,框架需要一个接口,允许用户自定义功能,并嵌入到框架当中,仿佛这个功能就是框架原生支持的一样。因此,对中间件而言,需要考虑以下两个关键的点:

  • 插入点在哪?使用框架的人不关心底层逻辑的具体实现,如果插入点太底层,中间件的逻辑就会非常复杂。如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 种调用相比没有太大的优势;
  • 中间件的输入是什么?中间件的输入,决定了拓展能力。暴露的参数太少,用户发挥的空间有限。

Gee 参考 Gin,设计了中间件

中间件设计

Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是 Context 对象。插入点是框架接收到请求初始化 Context 对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对 Context 进行二次加工

另外通过调用(*Context).Next()函数,中间件可等待用户自定义的 Handler 结束后,再做一些额外的操作,例如计算本次处理所用时间等。即 Gee 的中间件支持用户在请求处理的前后,做一些额外的操作。

例如,我们希望最终支持如下定义的中间件c.Next()表示等待执行其它的中间件或用户的 Handler:

func Logger() HandlerFunc {return func(c *Context) {// Start timert := time.Now()// Process requestc.Next()// Calculate resolution timelog.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))}
}

另外,支持设置多个中间件,依次进行调用。

在昨天的分组控制中提到,中间件是应用在 RouterGroup 上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。

按照之前我们设计 Gee 框架的逻辑,当接收到请求之后,首先进行路由匹配,该请求的所有信息都将会存储在 Context 当中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在 Context 中,依次进行调用。**保存在 Context 当中的原因是,**在设计中,中间件不仅作用在处理流程前,还有可能作用在处理流程后,即在用户定义的 Handler 处理完毕后,还可以执行剩下的操作。

为此,我们给 Context 添加两个参数,定义 Next 方法:

type Context struct {// origin objects// 封装了 http.ResponseWriter 和 *http.RequestWriter http.ResponseWriterReq    *http.Request// request infoPath   stringMethod stringParams map[string]string// response infoStatusCode int// middlewarehandlers []HandlerFuncindex    int
}func newContext(w http.ResponseWriter, req *http.Request) *Context {return &Context{Writer: w,Req:    req,Path:   req.URL.Path,Method: req.Method,index:  -1,}
}func (c *Context) Next() {c.index++s := len(c.handlers)for ; c.index < s; c.index++ {c.handlers[c.index](c)}
}

index 记录的是当前执行到了第几个中间件,当在中间件中调用 Next 方法,控制权就交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在 Next 方法之后定义的部分。

这部分内容直接参考了 Geektutu 大佬博客的原文:
在这里插入图片描述
最后一句话是重点,最终的调用顺序是part1 -> part3 -> Handler -> part4 -> part2。原因是在执行 part1 之后调用了 Next,在 part3 之后也调用了 Next。

代码实现

定义 Use 函数,将中间件应用到某个 Group:

func (group *RouterGroup) Use(middlewares ...HandlerFunc) {group.middlewares = append(group.middlewares, middlewares...)
}func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {var middlewares []HandlerFuncfor _, group := range engine.groups {if strings.HasPrefix(req.URL.Path, group.prefix) {middlewares = append(middlewares, group.middlewares...)}}c := newContext(w, req)c.handlers = middlewaresengine.router.handle(c)
}

ServeHTTP 函数也有一些变换,当我们接收到一个具体请求时,需要判断该请求适用于哪些中间件,在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers

在 handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers 列表中,执行 c.Next()

func (r *router) handle(c *Context) {n, params := r.getRoute(c.Method, c.Path)if n != nil {key := c.Method + "-" + n.patternc.Params = paramsc.handlers = append(c.handlers, r.handlers[key])} else {c.handlers = append(c.handlers, func(c *Context) {c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)})}c.Next()
}

Demo

注意,首先我们应该在gee/context.go中补充 *Context 对象的 Fail 方法:

func (c *Context) Fail(code int, err string) {c.index = len(c.handlers)c.JSON(code, H{"message": err})
}

之后更新 main 函数:

package mainimport ("gee/gee""log""net/http""time"
)func onlyForV2() gee.HandlerFunc {return func(c *gee.Context) {// Start timert := time.Now()// if a server error occurredc.Fail(500, "Internal Server Error")// Calculate resolution timelog.Printf("[%d] %s in %v for group v2", c.StatusCode, c.Req.RequestURI, time.Since(t))}
}func main() {r := gee.New()r.Use(gee.Logger()) // global middlewarer.GET("/", func(c *gee.Context) {c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")})v2 := r.Group("/v2")v2.Use(onlyForV2()) // v2 group middleware{v2.GET("/hello/:name", func(c *gee.Context) {// expect /hello/geektutuc.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)})}r.Run(":9999")
}

由于我们使用了 OnlyForV2 中间件,因此在访问/hello/:name:name是可选的参数)时,浏览器显示的结果如下:

{"message":"Internal Server Error"}

同时,命令行将会记录日志信息:

2025/02/22 08:19:06 [200] /v2/hello/123 in 0s

v2.Use(onlyForV2()) // v2 group middleware注释掉即可正常显示,验证了中间件的有效性。


http://www.ppmy.cn/server/170320.html

相关文章

医疗AI领域中GPU集群训练的关键技术与实践经验探究(下)

五、医疗 AI 中 GPU 集群架构设计 5.1 混合架构设计 5.1.1 参数服务器与 AllReduce 融合 在医疗 AI 的 GPU 集群训练中,混合架构设计将参数服务器(Parameter Server)与 AllReduce 相结合,能够充分发挥两者的优势,提升训练效率和模型性能。这种融合架构的设计核心在于根…

DAY12 Tensorflow过拟合

在模型的训练中&#xff0c;我们通常会遇到过拟合和欠拟合现象。 欠拟合的解决方法&#xff1a; 增加输入特征项 增加网络参数 减少正则化参数 过拟合的解决方法&#xff1a; 数据清洗 增大训练集 采用正则化 增大正则化参数 正则化通常只对W使用&#xff0c;不对偏执值b使用…

单例模式【C++设计模式】

文章目录 单例模式饿汉模式懒汉模式 单例场景 单例模式 饿汉模式 将构造函数设置为私有&#xff0c;并将拷贝构造函数和赋值运算符重载函数设置为私有或删除&#xff0c;防止外部创建或拷贝对象。提供一个指向单例对象的static指针&#xff0c;并在程序入口之前完成单例对象的…

Python--函数进阶(上)

1. 参数深入理解 1.1 参数传递的内存机制 Python中参数传递的是内存地址&#xff08;引用传递&#xff09;&#xff0c;而非值拷贝。这意味着&#xff1a; 可变对象&#xff08;列表、字典&#xff09;在函数内修改会影响外部变量。不可变对象&#xff08;数字、字符串&…

【从零开始学Redis】高级篇--超全总结笔记

Redis从入门到精通&#xff0c;超全笔记专栏 Hello&#xff0c;大家好&#xff0c;我是姜来可期&#xff08;全网同名&#xff09;&#xff0c;一名在读985转码大学生&#xff0c;一起学习&#xff0c;共同交流&#xff01;&#xff01;&#xff01; 分布式缓存 分布式缓存是…

5. Go 方法(结构体的方法成员)

Go语言没有传统的 class &#xff0c;为了让函数和结构体能够关联&#xff0c;Go引入了“方法”的概念。 当普通函数添加了接收者&#xff08;receiver&#xff09;后&#xff0c;就变成了方法。 一、函数和方法示例 // 普通函数 func Check(s string) string {return s }//…

内网网络安全的解决之道

本文简要分析了企业内部网络所面临的主要分析&#xff0c;阐述了安全管理人员针对不同威胁的主要技术应对措施。进一步介绍了业界各种技术措施的现状&#xff0c;并提出了未来可能的发展趋势。 内网网络安全问题的提出 网络安全对于绝大多数人而言指的都是互联网安全&#xff…

【DeepSeek与鸿蒙HarmonyOS:开启应用开发新次元】

引言&#xff1a;科技融合的新曙光 在当今数字化浪潮中&#xff0c;DeepSeek 和鸿蒙 HarmonyOS 宛如两颗璀璨的明星&#xff0c;各自在人工智能和操作系统领域熠熠生辉。DeepSeek 以其强大的大模型能力&#xff0c;在自然语言处理、代码生成等多个领域展现出卓越的性能&#x…