4、中间件开发:全局中间件、局部中间件、自定义中间件、中间件执行顺序、常见中间件实现(日志、鉴权、限流)
中间件这东西,说白了就是请求处理链路上的一个个关卡。每个请求进来,先过一遍中间件,再到你的业务逻辑。我刚开始接触Go Web框架时,总觉得中间件很神秘,后来自己手写了几套,才发现核心思想其实很简单——就是一个函数嵌套调用。
4.1 全局中间件 vs 局部中间件
先搞清楚这两个概念。全局中间件,就是所有路由都会经过的中间件。局部中间件,只挂载到特定路由或路由组上。
我个人习惯这样划分:
- 全局中间件:日志记录、恢复panic、请求ID注入
- 局部中间件:鉴权(某些接口不需要)、限流(不同接口不同策略)
举个例子,你想想看,登录接口肯定不能走鉴权中间件,否则用户还没登录就被拦住了。这就是局部中间件的典型场景。
核心区别:全局中间件影响所有路由,局部中间件只影响特定路由。选择依据是「这个逻辑是否对所有接口都适用」。
4.2 自定义中间件开发
在Go里写一个中间件,本质上就是写一个函数,返回一个HandlerFunc。我常用的模式是这样的:
// 标准中间件签名
type Middleware func(HandlerFunc) HandlerFunc
// 自定义中间件示例:请求耗时统计
func TimingMiddleware(next HandlerFunc) HandlerFunc {
return func(c *Context) {
start := time.Now()
// 这里可以做前置处理
next(c) // 调用下一个中间件或业务逻辑
// 这里可以做后置处理
elapsed := time.Since(start)
log.Printf("[Timing] %s %s took %v", c.Request.Method, c.Request.URL.Path, elapsed)
}
}
嗯,这里要注意一个细节:next(c) 调用前后的代码,分别对应请求进入和响应返回时的处理逻辑。我在项目中遇到过一个问题——有人在 next(c) 之后修改了响应体,结果发现根本改不了。为什么?因为响应已经写出去了。
小技巧:如果你需要在中间件里修改响应内容,可以用自定义的ResponseWriter包装一下,拦截Write方法。Gin框架的Logger中间件就是这么干的。
4.3 中间件执行顺序
这个坑我踩过。中间件的执行顺序,其实就是洋葱模型。请求进来时,按注册顺序执行前置逻辑,然后一层层往里走,到达业务逻辑后,再按相反顺序执行后置逻辑。
看个例子就明白了:
// 注册顺序:A -> B -> C
r.Use(MiddlewareA)
r.Use(MiddlewareB)
r.Use(MiddlewareC)
// 执行顺序:
// 请求进入 -> A前置 -> B前置 -> C前置 -> 业务逻辑 -> C后置 -> B后置 -> A后置 -> 响应返回
我曾经在项目里把限流中间件放在了日志中间件后面,结果限流拒绝的请求也被记录到了日志里。后来调整了顺序,把限流放在最前面,日志放在后面,这样被限流的请求就不会产生日志了。
避坑指南:我曾经因为中间件顺序搞反,导致panic恢复中间件没有包裹住所有业务逻辑,一个空指针panic直接让服务挂了。记住:恢复中间件一定要放在最外层(最先注册)。
4.4 常见中间件实现
4.4.1 日志中间件
日志中间件是最基础的。我一般会记录:请求方法、路径、状态码、耗时、客户端IP。
func LoggerMiddleware(next HandlerFunc) HandlerFunc {
return func(c *Context) {
start := time.Now()
path := c.Request.URL.Path
method := c.Request.Method
clientIP := c.ClientIP()
next(c)
latency := time.Since(start)
statusCode := c.StatusCode
log.Printf("[%d] %s %s %v %s", statusCode, method, path, latency, clientIP)
}
}
你想想看,如果每个接口都自己写日志,那得多累。中间件一次搞定,所有接口自动就有了日志能力。
4.4.2 鉴权中间件
鉴权中间件,说白了就是检查请求里有没有合法的凭证。我常用的做法是JWT鉴权:
func AuthMiddleware(next HandlerFunc) HandlerFunc {
return func(c *Context) {
token := c.Request.Header.Get("Authorization")
if token == "" {
c.JSON(401, map[string]string{"error": "missing token"})
return // 注意:这里不调用 next,直接中断
}
claims, err := ParseJWT(token)
if err != nil {
c.JSON(401, map[string]string{"error": "invalid token"})
return
}
// 把用户信息注入到上下文
c.Set("user_id", claims.UserID)
c.Set("role", claims.Role)
next(c) // 鉴权通过,继续执行
}
}
这里有个关键点:鉴权失败时,直接返回响应,不再调用 next(c)。这样就实现了请求拦截。我在项目中遇到过一个问题——有人鉴权失败后还调用了 next(c),结果业务逻辑也执行了,返回了两次响应,客户端直接报错。
重要原则:中间件里如果决定中断请求,直接返回即可,不要调用 next(c)。否则后续中间件和业务逻辑都会执行,造成不可预期的结果。
4.4.3 限流中间件
限流中间件,我常用的是令牌桶算法。说白了就是控制请求的速率,防止服务被打爆。
func RateLimitMiddleware(rate int, burst int) Middleware {
limiter := rate.NewLimiter(rate.Limit(rate), burst)
return func(next HandlerFunc) HandlerFunc {
return func(c *Context) {
if !limiter.Allow() {
c.JSON(429, map[string]string{"error": "too many requests"})
return
}
next(c)
}
}
}
// 使用:每秒10个请求,突发20个
r.Use(RateLimitMiddleware(10, 20))
嗯,这里要注意,限流中间件通常放在最外层。为什么?因为限流要尽早拦截,避免无效请求消耗后端资源。我见过有人把限流放在日志后面,结果被限流的请求也写了日志,白白浪费IO。
4.5 中间件组合与复用
实际项目中,中间件经常需要组合使用。我习惯把常用的中间件组合封装起来:
// 通用中间件组合
func CommonMiddleware() []Middleware {
return []Middleware{
RecoveryMiddleware(), // 恢复panic
LoggerMiddleware(), // 日志
RequestIDMiddleware(), // 请求ID
}
}
// 管理后台中间件组合
func AdminMiddleware() []Middleware {
return []Middleware{
AuthMiddleware(), // 鉴权
PermissionMiddleware(), // 权限校验
AuditMiddleware(), // 操作审计
}
}
// 使用
r.Use(CommonMiddleware()...)
adminGroup := r.Group("/admin", AdminMiddleware()...)
你想想看,这样组织代码,是不是清晰多了?每个中间件只做一件事,组合起来就能完成复杂的功能。这就是中间件的魅力所在。
个人建议:中间件尽量保持无状态,不要依赖全局变量。如果非要传递数据,通过Context的Set/Get方法,或者使用依赖注入的方式。这样测试起来也方便。
4.6 总结
中间件开发,核心就三点:
- 理解洋葱模型:前置逻辑和后置逻辑的执行顺序
- 掌握中断机制:不调用next就是中断请求
- 注意注册顺序:先注册的在外层,后注册的在内层
我在项目中见过太多因为中间件顺序搞错而引发的线上事故。嗯,说白了,中间件就是一层皮,包得好,系统稳如泰山;包得不好,bug层出不穷。希望你能把这些经验用起来,少踩一些坑。