装饰器模式 是实现功能解耦和动态扩展的核心设计模式,而 洋葱模型 作为装饰器模式的链式进阶,是 Web 框架中间件、RPC 拦截器的底层核心。
本文介绍用 Go 实现支持 context.Context 上下文传递的装饰器模式(包括函数式和接口式两种实现),并进阶实现洋葱模型
一、装饰器模式#
模式定义#
装饰器模式是一种结构型设计模式,核心原则:不修改原有代码逻辑,动态为对象/函数添加额外功能。
它遵循开放封闭原则:对扩展开放,对修改关闭。
核心特性#
- 动态增强:运行期为目标添加前置/后置逻辑
- 无侵入性:不改动核心业务代码
- 可组合:多个装饰器链式叠加,自由组合功能
- 上下文透传:配合
context.Context 实现全链路数据传递(超时、请求ID、用户信息等)
适用场景#
- 日志打印、耗时统计
- 权限校验、参数校验
- 缓存、重试、限流熔断
- 全链路上下文管理
二、Go 实现装饰器模式(带 Context 上下文)#
函数式装饰器#
Go 没有类继承,通过函数包装实现装饰器是最简洁的方案。
为装饰器加入 context.Context,实现上下文全链路传递(核心需求)。
设计思路#
- 定义统一的业务函数类型(强制携带 Context)
- 编写核心业务函数(无任何增强逻辑)
- 编写装饰器函数:接收原函数 → 返回包装后的新函数
- 装饰器内部通过 Context 传递/读取数据,实现全链路交互
函数式装饰器代码示例#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
package main
import (
"context"
"fmt"
"time"
)
// 定义业务函数类型:携带 context.Context,支持上下文传递
// 这是装饰器的统一接口
type BusinessFunc func(ctx context.Context, name string) string
// 原始核心业务函数:仅实现核心逻辑,无增强代码
func SayHello(ctx context.Context, name string) string {
// 从上下文读取中间件传递的数据
reqID, _ := ctx.Value("req_id").(string)
user, _ := ctx.Value("user").(string)
fmt.Printf("[核心业务] 请求ID:%s | 操作用户:%s | 执行核心逻辑\n", reqID, user)
return fmt.Sprintf("你好,%s!", name)
}
// 装饰器1:日志装饰器(携带上下文)
func LogDecorator(f BusinessFunc) BusinessFunc {
return func(ctx context.Context, name string) string {
// 前置增强:打印请求日志
reqID := ctx.Value("req_id").(string)
fmt.Printf("[日志装饰器] 请求ID:%s | 开始调用,参数:%s\n", reqID, name)
// 调用原函数(透传上下文)
result := f(ctx, name)
// 后置增强:打印响应日志
fmt.Printf("[日志装饰器] 请求ID:%s | 调用结束,结果:%s\n", reqID, result)
return result
}
}
// 装饰器2:计时装饰器(携带上下文)
func TimeDecorator(f BusinessFunc) BusinessFunc {
return func(ctx context.Context, name string) string {
start := time.Now()
reqID := ctx.Value("req_id").(string)
// 调用原函数(透传上下文)
result := f(ctx, name)
// 后置增强:打印耗时
fmt.Printf("[计时装饰器] 请求ID:%s | 执行耗时:%s\n", reqID, time.Since(start))
return result
}
}
// 装饰器3:上下文初始化装饰器(往ctx存入数据,供下游使用)
func ContextDecorator(f BusinessFunc) BusinessFunc {
return func(ctx context.Context, name string) string {
// 给上下文添加请求ID、用户信息(全链路透传)
ctx = context.WithValue(ctx, "req_id", "REQ_123456")
ctx = context.WithValue(ctx, "user", "admin")
fmt.Println("[上下文装饰器] 初始化上下文完成")
return f(ctx, name)
// 无后置增强
}
}
func main() {
// 链式装饰:顺序 = 上下文装饰 → 日志装饰 → 计时装饰
decoratedFunc := ContextDecorator(LogDecorator(TimeDecorator(SayHello)))
// 根上下文
ctx := context.Background()
// 执行增强后的函数
res := decoratedFunc(ctx, "张三")
fmt.Println("\n最终返回结果:", res)
}
|
运行结果#
注意每层装饰器中被装饰函数 BusinessFunc 的调用位置,后面的代码是被装饰函数返回后再执行的
1
2
3
4
5
6
7
|
[上下文装饰器] 初始化上下文完成
[日志装饰器] 请求ID:REQ_123456 | 开始调用,参数:张三
[核心业务] 请求ID:REQ_123456 | 操作用户:admin | 执行核心逻辑
[计时装饰器] 请求ID:REQ_123456 | 执行耗时:22.083µs
[日志装饰器] 请求ID:REQ_123456 | 调用结束,结果:你好,张三!
最终返回结果: 你好,张三!
|
执行流程#
graph TD
A[调用 decoratedFunc] --> B[ContextDecorator 注入上下文]
B --> C[调用 LogDecorator]
C --> D[打印前置日志]
D --> F[调用 TimeDecorator]
F --> G[记录开始时间]
G --> H[调用核心业务 SayHello]
H --> I[返回TimeDecorator执行后置代码]
I --> J[返回LogDecorator执行后置代码]
J --> K[返回最终结果]
接口式装饰器#
除了函数式装饰器,Go 中还可以通过接口实现装饰器模式,这种方式更适合面向对象的场景。
接口式装饰器设计思路#
- 定义业务接口
- 实现基础业务结构体
- 实现装饰器结构体(包装基础业务)
- 通过组合实现功能增强
接口式装饰器示例代码#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
package main
import (
"context"
"fmt"
"time"
)
// 1. 定义业务接口
type Service interface {
Process(ctx context.Context, input string) (string, error)
}
// 2. 基础业务实现
type BasicService struct{}
func (s *BasicService) Process(ctx context.Context, input string) (string, error) {
reqID := ctx.Value("req_id").(string)
user := ctx.Value("user").(string)
fmt.Printf("[基础服务] 请求ID:%s | 用户:%s | 处理输入:%s\n",
reqID, user, input)
// 业务处理
time.Sleep(10 * time.Millisecond)
return fmt.Sprintf("业务参数:%s", input), nil
}
// 3. 日志装饰器(接口式)
type LogDecorator struct {
service Service
}
func (d *LogDecorator) Process(ctx context.Context, input string) (string, error) {
reqID := ctx.Value("req_id").(string)
// 前置日志
fmt.Printf("[日志装饰器] 请求ID:%s | 开始处理\n", reqID)
start := time.Now()
result, err := d.service.Process(ctx, input)
duration := time.Since(start)
// 后置日志
fmt.Printf("[日志装饰器] 请求ID:%s | 处理完成 | 耗时:%v\n",
reqID, duration)
return result, err
}
// 4. 权限装饰器(接口式)
type AuthDecorator struct {
service Service
}
func (d *AuthDecorator) Process(ctx context.Context, input string) (string, error) {
reqID := ctx.Value("req_id").(string)
user := ctx.Value("user").(string)
// 权限检查
if user == "" {
return "", fmt.Errorf("请求ID %s: 用户未认证", reqID)
}
fmt.Printf("[权限装饰器] 请求ID:%s | 用户 %s 权限校验通过\n",
reqID, user)
return d.service.Process(ctx, input)
}
// 5. 上下文装饰器(接口式)
type ContextDecorator struct {
service Service
}
func (d *ContextDecorator) Process(ctx context.Context, input string) (string, error) {
// 注入请求级数据
ctx = context.WithValue(ctx, "req_id", "REQ_"+time.Now().Format("150405"))
ctx = context.WithValue(ctx, "user", "admin")
fmt.Println("[上下文装饰器] 初始化全链路数据")
return d.service.Process(ctx, input)
}
func main() {
// 创建基础服务
var service Service = &BasicService{}
// 链式装饰:上下文 → 权限 → 日志 → 基础服务
service = &ContextDecorator{service: &AuthDecorator{service: &LogDecorator{service: &BasicService{}}}}
// 等价于
// service = &LogDecorator{service: service}
// service = &AuthDecorator{service: service}
// service = &ContextDecorator{service: service}
ctx := context.Background()
result, err := service.Process(ctx, "test_input")
if err != nil {
fmt.Printf("错误:%v\n", err)
} else {
fmt.Printf("结果:%s\n", result)
}
}
|
运行结果#
1
2
3
4
5
6
|
[上下文装饰器] 初始化全链路数据
[权限装饰器] 请求ID:REQ_140913 | 用户 admin 权限校验通过
[日志装饰器] 请求ID:REQ_140913 | 开始处理
[基础服务] 请求ID:REQ_140913 | 用户:admin | 处理输入:test_input
[日志装饰器] 请求ID:REQ_140913 | 处理完成 | 耗时:11.032334ms
结果:业务参数:test_input
|
接口式装饰器特点#
- 面向对象:通过结构体组合实现装饰器
- 类型安全:编译时检查接口实现
- 易于扩展:新增装饰器只需实现相同接口
洋葱模型:装饰器的链式进阶(带 Context)#
洋葱模型核心原理#
其实前面的示例中就已经包含了洋葱模型,洋葱模型是多层装饰器的标准编排模式,执行流程:
请求从外层进入 → 层层穿透到核心业务 → 响应从核心层层向外退出
配合 context.Context,可实现全链路上下文共享(请求ID、超时、用户信息贯穿所有层)。
执行流程图解#
graph LR
A[请求进入] --> B[外层中间件 前置逻辑]
B --> C[内层中间件 前置逻辑]
C --> D[核心业务]
D --> E[内层中间件 后置逻辑]
E --> F[外层中间件 后置逻辑]
F --> G[响应返回]
style B fill:#e1f5fe
style C fill:#f3e5f5
style D fill:#e8f5e8
style E fill:#f3e5f5
style F fill:#e1f5fe
Go 实现带 Context 的洋葱模型#
这里用常见的中间件写法实现一个简单的示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
package main
import (
"context"
"fmt"
)
// 1. 核心处理器:携带Context,业务最终执行点
type Handler func(ctx context.Context)
// 2. 中间件类型:洋葱模型的核心,接收Handler,返回新Handler(带Context)
type Middleware func(next Handler) Handler
// 3. 中间件1:请求日志(外层洋葱)
func LoggerMiddleware(next Handler) Handler {
return func(ctx context.Context) {
// 前置逻辑:进入
reqID := ctx.Value("req_id").(string)
fmt.Printf("→ [日志中间件] 请求ID:%s | 接收请求\n", reqID)
// 调用下一层(穿透到内层)
next(ctx)
// 后置逻辑:退出
fmt.Printf("← [日志中间件] 请求ID:%s | 处理完成\n", reqID)
}
}
// 4. 中间件2:权限校验(内层洋葱)
func AuthMiddleware(next Handler) Handler {
return func(ctx context.Context) {
reqID := ctx.Value("req_id").(string)
user := ctx.Value("user").(string)
// 前置逻辑:权限校验
fmt.Printf(" → [权限中间件] 请求ID:%s | 校验用户:%s 权限 ✅\n", reqID, user)
// 调用核心业务
next(ctx)
// 后置逻辑
fmt.Printf(" ← [权限中间件] 请求ID:%s | 权限校验结束\n", reqID)
}
}
// 5. 中间件3:上下文初始化(最外层,注入公共数据)
func ContextMiddleware(next Handler) Handler {
return func(ctx context.Context) {
// 初始化上下文:注入请求ID、用户、超时时间
ctx = context.WithValue(ctx, "req_id", "REQ_789012")
ctx = context.WithValue(ctx, "user", "test_user")
fmt.Println("[上下文中间件] 初始化全链路上下文")
next(ctx)
}
}
// 6. 核心业务逻辑(洋葱最内层)
func coreBusiness(ctx context.Context) {
reqID := ctx.Value("req_id").(string)
user := ctx.Value("user").(string)
fmt.Printf(" ✅ [核心业务] 请求ID:%s | 用户:%s | 执行核心逻辑\n", reqID, user)
}
// 7. 洋葱组装函数:将中间件按顺序包装成最终Handler
func compose(handler Handler, middlewares ...Middleware) Handler {
// 倒序包装:从内到外,层层包裹
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
func main() {
// 组装洋葱:顺序 = 上下文 → 日志 → 权限
finalHandler := compose(coreBusiness, ContextMiddleware, LoggerMiddleware, AuthMiddleware)
// 根上下文
ctx := context.Background()
finalHandler(ctx)
}
|
洋葱模型运行结果#
1
2
3
4
5
6
|
[上下文中间件] 初始化全链路上下文
→ [日志中间件] 请求ID:REQ_789012 | 接收请求
→ [权限中间件] 请求ID:REQ_789012 | 校验用户:test_user 权限 ✅
✅ [核心业务] 请求ID:REQ_789012 | 用户:test_user | 执行核心逻辑
← [权限中间件] 请求ID:REQ_789012 | 权限校验结束
← [日志中间件] 请求ID:REQ_789012 | 处理完成
|
洋葱模型执行流程(序列图)#
sequenceDiagram
participant A as 客户端
participant CM as ContextMiddleware
participant LM as LoggerMiddleware
participant AM as AuthMiddleware
participant CB as CoreBusiness
A->>CM: 请求到达
Note over CM: 初始化上下文数据
CM->>LM: 调用下一层
Note over LM: 前置日志
LM->>AM: 调用下一层
Note over AM: 权限校验
AM->>CB: 调用核心业务
Note over CB: 执行业务逻辑
CB-->>AM: 返回结果
Note over AM: 权限校验后置
AM-->>LM: 返回结果
Note over LM: 后置日志
LM-->>CM: 返回结果
CM-->>A: 返回最终响应
应用场景#
-
Web 框架中间件:Gin/Beego 中间件底层就是洋葱模型 + Context
-
RPC 服务拦截器:gRPC 拦截器、微服务全链路监控
-
全链路监控:通过 Context 传递 TraceID、请求ID,实现日志串联
-
通用切面功能:超时控制、限流、熔缓存统一处理
遵循开闭原则,解耦核心业务与通用功能,代码更易维护、扩展
核心概念#
- 装饰器模式:Go 支持函数式和接口式,配合 Context 实现上下文传递
- 洋葱模型:装饰器的链式编排,先进后出,是中间件的标准实现
选择建议#
- 简单场景:使用函数式装饰器,更轻量、更符合Go惯用法
- 复杂对象:使用接口式装饰器,类型安全、易于扩展
- Web框架:使用洋葱模型实现中间件
掌握这两种模式能轻松实现通用切面逻辑,写出更优雅、更健壮的代码!