背景&问题
目前手动在写一个框架, 遇到一个很有意思的事情, 特此记录&分享一下
先看如下代码
// AddMiddleware 增加中间件
func (app *App) AddMiddleware(middlewares ...gin.HandlerFunc) {
app.Middlewares = append(app.Middlewares, middlewares...)
}
// RemoveMiddleware 移除中间件
func (app *App) RemoveMiddleware(middlewares ...gin.HandlerFunc) {
// 用户可以指定不使用某个中间件
// WARNING 感觉这段代码有风险, 因为go的函数不能比较,有可能地址一样,但是行为不同,比如闭包
// 但是对于中间件,一般都是在初始化的时候就确定了,不会在运行时动态改变,所以认为是安全的
// 如果针对函数一定要比较,可以封装一个标识符
// 每次调用都会返回 比如 ResponseMiddleware()都会返回一个新的匿名函数实例, 因此下面做法就是失败,而且风险大
for _, middleware := range middlewares {
sf1 := reflect.ValueOf(middleware)
for i := 0; i < len(app.Middlewares); i++ {
sf2 := reflect.ValueOf(app.Middlewares[i])
if sf1.Pointer() == sf2.Pointer() {
app.Middlewares = slices.Delete(app.Middlewares, i, i+1)
i-- // 调整索引
}
}
}
}
想法很简单,就是实现一个增加和移除中间件的功能, 框架自身初始化时候会默认带上某些中间件(比如response标准化输出),但是呢万一用户又不想使用,因此控制权交给用户,让用户自主选择。
但是自己在测试时候发现,RemoveMiddleware即使用了反射一直不生效,因此排查代码,发现函数中使用匿名函数实现,这样每次调用ResponseMiddleware函数,拿到的函数地址肯定不一样
// ResponseMiddleware 中间件:自动封装响应
func ResponseMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
...
}
}
事已至此,就发现问题,接下来就要看如何改动,成本低,风险小,更通用
解决方案
1. 修改匿名函数为全局函数
此方案呢,评估改动最小,后续所有中间件都定义全局变量,RemoveMiddleware代码天然复用
// ResponseMiddleware 中间件:自动封装响应
func ResponseMiddleware() gin.HandlerFunc {
return responseMiddleware
}
func responseMiddleware(c *gin.Context) {
c.Next()
}
但是还是不想用这玩意, 本身函数指针比较就是一个危险的事情,只能保证地址一样,不能保证函数行为一样
2. 给函数新增标识符
Middleware接口定义如下,通过name去判断,函数是否一致
type Middleware interface {
Handler() gin.HandlerFunc
Identifier() string
}