在Go语言开发中,我们经常遇到这样的需求:为一个已经写好的核心逻辑动态添加日志、监控、重试或缓存等能力,却又不想直接修改原有函数导致代码耦合。装饰者模式提供了一种优雅的方案,它通过组合而非继承来扩展对象行为。在Golang中,由于没有类的继承体系,我们主要依赖interface和结构体嵌套来实现这一模式。

一、装饰者模式的核心概念
装饰者模式属于结构型设计模式,其意图是动态地给一个对象添加一些额外的职责。就增加功能来说,装饰者模式相比生成子类更为灵活。在传统的面向对象语言中,通常通过继承父类并重写方法来实现,但Go语言不支持继承,而是提倡使用接口和组合。
在Go里实现装饰者模式,关键三步是:第一,定义一个业务接口(interface),描述被装饰对象的核心行为;第二,编写一个基础结构体实现该接口,代表原始功能;第三,定义装饰结构体,它同样实现该接口,但在内部持有一个接口类型的字段,用于保存被装饰的对象,并在调用时前后插入增强逻辑。
这种方式的优势在于,装饰器和被装饰者都遵循同一套接口契约,调用方完全无感知。你可以一层层包裹,比如先加日志装饰器,再外包一层缓存装饰器,最终拿到的是一个具备了多种能力的接口实例,但对外方法签名始终不变。
二、基础接口与原始实现
假设我们有一个简单的消息发送服务,只负责把内容发出去。我们先定义接口和最基本的结构体。
package main
import "fmt"
// 定义消息发送接口
type Sender interface {
Send(msg string) error
}
// 基础实现:直接打印发送
type BaseSender struct{}
func (b *BaseSender) Send(msg string) error {
fmt.Println("发送消息:", msg)
return nil
}
上面的Sender接口只有一个Send方法,BaseSender是其最朴素的实现。现在如果希望每次发送都记录耗时,且当发送失败时自动重试,我们不必改动BaseSender,而是写装饰器。
注意这里接口方法返回的是error,装饰器可以利用这个返回值做重试判断。基础实现保持纯净,便于单独测试和复用,这也是单一职责原则的体现。
三、日志装饰器的实现
我们来实现第一个装饰者:给发送过程加上日志输出。它内部保存一个Sender接口,并在调用前后打印信息。
// 日志装饰器
type LogDecorator struct {
sender Sender
}
func NewLogDecorator(s Sender) Sender {
return &LogDecorator{sender: s}
}
func (l *LogDecorator) Send(msg string) error {
fmt.Println("[日志] 准备发送:", msg)
err := l.sender.Send(msg)
if err != nil {
fmt.Println("[日志] 发送失败:", err)
} else {
fmt.Println("[日志] 发送成功")
}
return err
}
在LogDecorator的Send方法中,我们先打印准备发送,然后调用内部sender的Send,最后根据错误打印结果。由于NewLogDecorator返回的是Sender接口,所以装饰后的对象仍可被当作普通Sender使用。
这种写法让日志逻辑与业务发送解耦。如果以后不想打日志了,只需在组装对象时不包这层装饰器即可,基础代码一行都不用动。对于需要统一埋点的团队来说,这是一种低侵入的增强方式。
四、重试装饰器的叠加
接着我们再加一个重试装饰器,展示多层装饰的效果。它会在发送失败时重试指定次数。
import "time"
// 重试装饰器
type RetryDecorator struct {
sender Sender
maxRetry int
}
func NewRetryDecorator(s Sender, max int) Sender {
return &RetryDecorator{sender: s, maxRetry: max}
}
func (r *RetryDecorator) Send(msg string) error {
var err error
for i := 0; i <= r.maxRetry; i++ {
err = r.sender.Send(msg)
if err == nil {
return nil
}
fmt.Println("第", i+1, "次重试失败:", err)
time.Sleep(time.Millisecond * 100)
}
return err
}
这里RetryDecorator同样持有Sender,并在循环中调用。当我们把BaseSender先交给日志装饰器,再交给重试装饰器,就得到了兼具两者能力的对象。
组合顺序会影响执行流程。如果先日志后重试,那么每次重试也会被打日志;如果先重试后日志,则日志只记录最外层最终结果。开发者应根据实际可观测性需求来安排包裹次序,这也是使用装饰者模式时容易忽略的细节。
五、组装与调用示例
下面在main函数中演示如何层层装饰并调用。
func main() {
base := &BaseSender{}
withLog := NewLogDecorator(base)
withLogAndRetry := NewRetryDecorator(withLog, 2)
// 使用最终装饰好的实例
_ = withLogAndRetry.Send("hello golang")
}
运行后你会看到日志装饰器输出了每次尝试的内容,而基础发送只负责最底层的动作。调用方main里只依赖Sender接口,完全不知道背后有几层包裹。
如果将来要新增一个“限流”能力,只需再写一个RateLimitDecorator,然后在组装时插入即可。整个系统对扩展开放,对修改封闭,符合开闭原则。
六、与中间件模式的区别及注意事项
有些开发者会混淆装饰者模式与HTTP中间件。中间件通常针对请求链路,依靠闭包和next函数显式传递;而Go装饰者模式基于接口,编译期就能检查方法签名是否匹配,类型更安全。
| 对比维度 | 装饰者模式 | 中间件链 |
|---|---|---|
| 实现基础 | interface与结构体 | 函数闭包 |
| 类型检查 | 编译期强类型 | 运行时约定 |
| 适用粒度 | 单个组件方法 | 请求处理流程 |
在使用装饰者模式时,要避免装饰器内部状态混乱。如果装饰器需要保存次数、缓存等字段,请确保其并发安全性。例如缓装饰器用map存结果时,应加sync.Mutex保护。
另外,不要过度包装。层数太多会让调用栈变深,出问题时日志难追溯。建议把多个弱相关逻辑合并到一个装饰器里,保持整体层级清晰,这样才能真正发挥该模式的价值。