Go中间件:递归组装与反向迭代组装

在 Go 后端开发中,中间件(Middleware)是承载日志、鉴权、限流、监控、Tracing 等横切逻辑的核心方案。它的价值在于:把通用增强逻辑与核心业务解耦,让功能可插拔、可组合、可扩展。

本文围绕一个完整可运行的"订单创建"示例,讲清楚两种经典的中间件链路组装方式:

  • 递归组装(ComposeRecursive):更贴合理论推导,易讲清"从后往前包裹"
  • 反向迭代组装(ComposeReverse):工程里更常用,边界更少、无递归栈

并用输出日志直观验证:调用链遵循洋葱模型(请求逐层进入,响应逐层返回)。


一、中间件链路的核心思想:从后往前包裹

我们把"服务"抽象为一个接口,把"中间件"抽象为一个包装器:输入 next 服务,返回增强后的服务。

  • 核心服务:S
  • 中间件:A、B、C(传入顺序为 [A, B, C]

最终组装结构(外到内)是:

less 复制代码
A( B( C( S ) ) )

执行顺序(洋葱模型)是:

  • 请求进入:A 前置 → B 前置 → C 前置 → S(核心逻辑)
  • 响应返回:S → C 后置 → B 后置 → A 后置

这种结构的好处是:你新增/移除/调整中间件顺序,不需要改核心业务代码


二、示例目标:订单服务 + 三层中间件(日志/鉴权/限流)

我们用一个最贴近业务的例子演示:

  • 核心服务:创建订单
  • 日志中间件:记录请求进入与结束、耗时、错误
  • 鉴权中间件:token 校验,失败短路
  • 限流中间件:每秒最多 N 次,超限短路(并发安全)

并分别用两种 Compose 方式组装链路,验证效果一致。


三、完整可运行代码(复制即可运行)

保存为 main.go,执行:go run main.go

go 复制代码
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

/*
	========================
	1) 抽象:服务 & 中间件
	========================
*/

type IOrderService interface {
	CreateOrder(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error)
}

type CreateOrderReq struct {
	UserID  string
	Token   string
	GoodsID string
	Amount  int64
}

type CreateOrderResp struct {
	OrderID string
	Success bool
}

type IOrderMiddleware interface {
	Wrap(next IOrderService) IOrderService
}

/*
	========================
	2) 核心业务服务
	========================
*/

type OrderService struct{}

func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error) {
	orderID := fmt.Sprintf("ORDER_%d", time.Now().UnixNano()/1e3) // 微秒级
	fmt.Printf("[核心订单服务] user=%s 创建订单成功 orderID=%s\n", req.UserID, orderID)

	return &CreateOrderResp{
		OrderID: orderID,
		Success: true,
	}, nil
}

/*
	========================
	3) 中间件实现:日志/鉴权/限流
	========================
*/

// --- 3.1 日志中间件(前置+后置)

type LogMiddleware struct{}

func (m *LogMiddleware) Wrap(next IOrderService) IOrderService {
	return &logOrderService{next: next}
}

type logOrderService struct {
	next IOrderService
}

func (l *logOrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error) {
	start := time.Now()
	fmt.Printf("[日志中间件] -> 请求进入 user=%s goods=%s at=%s\n",
		req.UserID, req.GoodsID, start.Format("2006-01-02 15:04:05.000"))

	resp, err := l.next.CreateOrder(ctx, req)

	cost := time.Since(start)
	orderID := ""
	if resp != nil {
		orderID = resp.OrderID
	}
	fmt.Printf("[日志中间件] <- 请求结束 orderID=%s cost=%v err=%v\n", orderID, cost, err)
	return resp, err
}

// --- 3.2 鉴权中间件(失败短路)

type AuthMiddleware struct {
	ValidToken string
}

func (m *AuthMiddleware) Wrap(next IOrderService) IOrderService {
	return &authOrderService{next: next, validToken: m.ValidToken}
}

type authOrderService struct {
	next       IOrderService
	validToken string
}

func (a *authOrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error) {
	if req.Token != a.validToken {
		fmt.Printf("[鉴权中间件] 鉴权失败 user=%s token=%s\n", req.UserID, req.Token)
		return &CreateOrderResp{Success: false}, fmt.Errorf("token invalid")
	}
	fmt.Printf("[鉴权中间件] 鉴权通过 user=%s\n", req.UserID)
	return a.next.CreateOrder(ctx, req)
}

// --- 3.3 限流中间件(每秒最多 N 次;并发安全)

type RateLimitMiddleware struct {
	MaxPerSec int

	mu      sync.Mutex
	window  time.Time
	counter int
}

func (m *RateLimitMiddleware) Wrap(next IOrderService) IOrderService {
	return &rateLimitOrderService{next: next, limiter: m}
}

type rateLimitOrderService struct {
	next    IOrderService
	limiter *RateLimitMiddleware
}

func (r *rateLimitOrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*CreateOrderResp, error) {
	now := time.Now()

	r.limiter.mu.Lock()
	defer r.limiter.mu.Unlock()

	// 以 1 秒为窗口
	if r.limiter.window.IsZero() || now.Sub(r.limiter.window) >= time.Second {
		r.limiter.window = now
		r.limiter.counter = 0
	}

	r.limiter.counter++
	if r.limiter.counter > r.limiter.MaxPerSec {
		fmt.Printf("[限流中间件] 请求超限 user=%s qps=%d limit=%d\n",
			req.UserID, r.limiter.counter, r.limiter.MaxPerSec)
		return &CreateOrderResp{Success: false}, fmt.Errorf("rate limit exceeded")
	}

	fmt.Printf("[限流中间件] 请求通过 user=%s qps=%d\n", req.UserID, r.limiter.counter)
	return r.next.CreateOrder(ctx, req)
}

/*
	========================
	4) Compose 两种写法
	========================
*/

// 4.1 递归写法:从后往前包裹
// middlewares[0] 最外层,middlewares[n-1] 最内层
func ComposeRecursive(core IOrderService, middlewares ...IOrderMiddleware) IOrderService {
	n := len(middlewares)
	if n == 0 {
		return core
	}
	// 先让"最后一个中间件"包裹 core,再递归组装剩余中间件
	last := middlewares[n-1]
	rest := middlewares[:n-1]
	return ComposeRecursive(last.Wrap(core), rest...)
}

// 4.2 反向迭代写法:从后往前包裹(工程上更常用)
// middlewares[0] 最外层,middlewares[n-1] 最内层
func ComposeReverse(core IOrderService, middlewares ...IOrderMiddleware) IOrderService {
	svc := core
	for i := len(middlewares) - 1; i >= 0; i-- {
		svc = middlewares[i].Wrap(svc)
	}
	return svc
}

/*
	========================
	5) main:分别测试两种 Compose 的效果一致
	========================
*/

func main() {
	core := &OrderService{}

	mws := []IOrderMiddleware{
		&LogMiddleware{},
		&AuthMiddleware{ValidToken: "VALID_TOKEN_2026"},
		&RateLimitMiddleware{MaxPerSec: 2},
	}

	fmt.Println("====================================================")
	fmt.Println("使用 ComposeRecursive(递归版)")
	fmt.Println("====================================================")
	svc1 := ComposeRecursive(core, mws...)
	runDemo(svc1)

	// 为了让两个 demo 互不影响(尤其限流窗口/计数),重新创建一套中间件实例
	mws2 := []IOrderMiddleware{
		&LogMiddleware{},
		&AuthMiddleware{ValidToken: "VALID_TOKEN_2026"},
		&RateLimitMiddleware{MaxPerSec: 2},
	}

	fmt.Println("\n====================================================")
	fmt.Println("使用 ComposeReverse(反向迭代版)")
	fmt.Println("====================================================")
	svc2 := ComposeReverse(core, mws2...)
	runDemo(svc2)
}

func runDemo(svc IOrderService) {
	fmt.Println("----- 合法请求 1 -----")
	req1 := &CreateOrderReq{UserID: "user_001", Token: "VALID_TOKEN_2026", GoodsID: "goods_1001", Amount: 99}
	resp1, err1 := svc.CreateOrder(context.Background(), req1)
	fmt.Printf("结果1 resp=%+v err=%v\n\n", resp1, err1)

	fmt.Println("----- 合法请求 2 -----")
	req2 := &CreateOrderReq{UserID: "user_001", Token: "VALID_TOKEN_2026", GoodsID: "goods_1002", Amount: 199}
	resp2, err2 := svc.CreateOrder(context.Background(), req2)
	fmt.Printf("结果2 resp=%+v err=%v\n\n", resp2, err2)

	fmt.Println("----- 超限请求 3(同一秒内第3次)-----")
	req3 := &CreateOrderReq{UserID: "user_001", Token: "VALID_TOKEN_2026", GoodsID: "goods_1003", Amount: 299}
	resp3, err3 := svc.CreateOrder(context.Background(), req3)
	fmt.Printf("结果3 resp=%+v err=%v\n\n", resp3, err3)

	time.Sleep(time.Second) // 让限流窗口重置,便于演示鉴权失败(不被限流挡住)

	fmt.Println("----- 非法请求 4(Token错误)-----")
	req4 := &CreateOrderReq{UserID: "user_002", Token: "INVALID_TOKEN_2026", GoodsID: "goods_1004", Amount: 399}
	resp4, err4 := svc.CreateOrder(context.Background(), req4)
	fmt.Printf("结果4 resp=%+v err=%v\n", resp4, err4)
}

四、两种 Compose 的组装过程详解

1)递归版 ComposeRecursive:更"像推导题"

关键点:每次取最后一个中间件 last,先包裹当前 core,然后把结果当成新的 core 递归下去:

go 复制代码
return ComposeRecursive(last.Wrap(core), rest...)

[Log, Auth, RateLimit]Core 来说:

  1. RateLimit(Core)
  2. Auth(RateLimit(Core))
  3. Log(Auth(RateLimit(Core)))

最终仍是 Log(Auth(RateLimit(Core)))

2)反向迭代版 ComposeReverse:更"工程化"

关键点:从后往前遍历中间件数组,不断把 svc 重新赋值为"被包裹后的服务"。

go 复制代码
svc := core
for i := len(middlewares)-1; i >= 0; i-- {
    svc = middlewares[i].Wrap(svc)
}
return svc

结构完全一致,但实现更直观、边界更少,也不会产生递归调用栈。


五、调用链可视化:洋葱模型

中间件传入顺序:日志 -> 鉴权 -> 限流

组装后结构(外 → 内):

markdown 复制代码
日志(
  鉴权(
    限流(
      核心订单服务
    )
  )
)

一次成功请求的执行顺序:

  1. 日志:打印"请求进入"
  2. 鉴权:校验 token
  3. 限流:计数 + 判断阈值
  4. 核心服务:创建订单
  5. 日志:打印"请求结束"(包含耗时、错误)

当鉴权失败或限流超限时:会短路返回,不会进入更内层服务。

相关推荐
初次攀爬者1 小时前
Redis脑裂问题处理——基于min-replicas-to-write配置
redis·后端
酱油瓶1 小时前
使用LangGraph4j/Spring AI构建智能问诊Agent
后端
用户0883361837932 小时前
JVM内存结构与类加载机制
后端
ding_zhikai2 小时前
【Web应用开发笔记】Django笔记3-2:部署我的简陋网页
笔记·后端·python·django
掘金者阿豪2 小时前
从MongoDB到金仓数据库:文档数据库国产化替代的实战路径与价值重构
后端
敲敲了个代码2 小时前
构建工具的第三次革命:从 Rollup 到 Rust Bundler,我是如何设计 robuild 的
开发语言·前端·javascript·后端·rust
Penge6662 小时前
Go反射练习:从复杂结构体中提取统一接口实例
后端
贾铭2 小时前
如何实现一个网页版的剪映(二)
前端·后端
troublea2 小时前
Laravel5.x核心特性全解析
数据库·spring boot·后端·mysql