Go中无装饰器语法,但可用高阶函数+闭包模拟,如WithTimeout、WithLogging等,返回新Handler实现日志、超时等功能;须正确传递context、保留原始error;HTTP推荐用chi等中间件,gRPC应直接使用UnaryServerInterceptor。

Go 里没有装饰器语法,但可以用函数值模拟
Go 语言本身不支持 Python 那种 @decorator 语法糖,也没有类方法装饰器的概念。所谓“Go 中的装饰器模式”,本质是用高阶函数(接收函数并返回新函数)封装行为增强逻辑,比如日志、重试、超时、熔断等。
关键在于:把要增强的业务逻辑抽象为 func() 或带参数/返回值的函数类型,再用闭包包装它。
- 典型签名:
type Handler func(ctx context.Context, req interface{}) (interface{}, error) - 装饰器函数接收
Handler,返回新的Handler,内部调用原函数前后插入逻辑 - 多个装饰器可链式组合:
WithTimeout(WithLogging(WithRetry(handler)))
用闭包实现日志和超时装饰器
装饰器必须能访问原始函数 + 外部配置(如超时时间、日志字段),闭包是最自然的载体。不要试图用结构体+方法模拟“类装饰器”,那会增加无谓复杂度。
示例:一个通用超时装饰器
func WithTimeout(d time.Duration) func(Handler) Handler {
return func(h Handler) Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
ctx, cancel := context.WithTimeout(ctx, d)
defer cancel()
return h(ctx, req)
}
}
}注意点:
-
context.WithTimeout返回的新ctx必须传给被装饰函数,否则超时无效 - 不能在装饰器里直接调用
h()而不传ctx,否则上下文传播中断 - 日志装饰器同理:用
log.Printf或zap.Logger记录入参/耗时/错误,但别在闭包外捕获 panic —— 那属于 recover 装饰器职责
避免嵌套过深和错误传播丢失
链式调用多个装饰器时,容易出现两层问题:一是调用栈变深影响性能(实际影响极小,可忽略);二是底层错误被中间装饰器吞掉或覆盖。
常见陷阱:
- 重试装饰器没把最后一次失败的
error返回,而是返回nil或固定错误 - 熔断装饰器在 open 状态下返回
circuit.ErrOpen,但上层没做类型断言或透传,导致业务逻辑误判 - 日志装饰器用
fmt.Printf打印错误后,返回了nil错误,掩盖真实失败原因
正确做法:所有装饰器必须原样返回被装饰函数的 error,仅在需要时 wrap(如用 fmt.Errorf("timeout: %w", err))并保留原始错误链。
HTTP handler 场景下更推荐用中间件而非手动链式调用
如果你在写 HTTP 服务,http.Handler 是标准接口,社区已有成熟中间件生态(如 chi.Mux、gorilla/mux 的 MiddlewareFunc)。此时硬套“装饰器函数链”反而绕路。
例如 chi 的写法更清晰:
r.Use(middleware.Logger)
r.Use(middleware.Timeout(5 * time.Second))
r.Get("/api/users", userHandler)它底层仍是装饰器思想,但隐藏了手动组合细节。自己造轮子前先确认是否真需要:是否已有框架支持?是否需跨协议复用(如同时用于 HTTP 和 gRPC)?
真正容易被忽略的是:gRPC 的 UnaryServerInterceptor 和 StreamServerInterceptor 本身就是装饰器模式的标准实现,别重复发明 —— 直接用 grpc.UnaryInterceptor() 注册即可。










