Go语言无原生装饰器语法,需用高阶函数或接口包装模拟:高阶函数适用于日志/超时等横切逻辑,需统一签名并注意执行顺序;接口包装用于HTTP中间件等运行时扩展,须显式转发调用且避免循环引用;不推荐反射或代码生成,因性能差、类型不安全;还需关注资源生命周期、错误链传递及业务边界权衡。

Go 语言没有原生的装饰器语法(比如 Python 的 @decorator),所以所谓“装饰器模式”只能靠函数式组合或接口嵌套来模拟——不是语法糖,而是设计意图的落地。
用高阶函数实现行为增强
最贴近“装饰器”直觉的做法是把函数作为参数传入另一个函数,返回增强后的新函数。适用于日志、重试、超时等横切逻辑。
- 必须显式调用装饰后的函数,不能自动拦截原始调用
- 被装饰函数签名需统一(如都接受
context.Context和返回error),否则类型不匹配 - 多个装饰器叠加时注意执行顺序:最外层的先执行,类似
log(decorate(retry(f)))
示例:
func withTimeout(f func(context.Context) error, timeout time.Duration) func(context.Context) error {
return func(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
return f(ctx)
}
}
// 使用
doWork := withTimeout(doWorkImpl, 5*time.Second)
err := doWork(ctx)
用接口+匿名结构体包装实现运行时扩展
当需要给已有类型动态加行为(比如给 http.Handler 加 CORS、JWT 验证),标准做法是定义接口,再写一个包装结构体,内部持有原对象并重写方法。
立即学习“go语言免费学习笔记(深入)”;
- 包装结构体字段名通常叫
next或handler,便于链式调用 - 必须显式调用
h.next.ServeHTTP(w, r),漏掉就中断链路 - 注意避免循环引用:不要在包装体里直接存自身指针
典型例子:
type loggingHandler struct {
next http.Handler
}
func (h *loggingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log.Printf("req: %s %s", r.Method, r.URL.Path)
h.next.ServeHTTP(w, r) // 关键:必须转发
}
为什么不用反射或代码生成做“自动装饰”
有人尝试用 reflect 动态代理方法,或用 go:generate 自动生成包装代码。实际项目中基本不推荐。
- 反射调用性能差,且丢失类型安全,IDE 无法跳转、静态检查失效
- 代码生成增加构建复杂度,每次改接口都要重新生成,调试困难
- Go 社区共识是“显式优于隐式”,装饰逻辑写清楚比藏在工具链里更可靠
容易被忽略的生命周期与错误处理
装饰器不只是加一层壳,它可能引入新资源(如 goroutine、timer、连接池)或改变错误传播路径。
- 如果装饰器启动了 goroutine,要确保它能随上下文取消而退出,否则引发 goroutine 泄漏
- 多个装饰器串联时,错误不应被吞掉;建议统一用
fmt.Errorf("xxx: %w", err)包装,保留原始 error 链 - HTTP 中间件类装饰器要注意
ResponseWriter是否被 wrap 过(比如ResponseWriter接口新增方法),某些库(如gorilla/handlers)已处理,自研需小心
真正难的从来不是怎么写一个装饰器,而是判断该不该在这里加——加在哪一层、是否影响可测试性、是否让调用栈变深到难以 debug。这些没法靠语法解决,得靠对业务边界的理解。


















