go中无原生decorator语法,需用签名一致的高阶函数模拟链式装饰,关键在统一context-aware类型、保持参数/返回值严格匹配、注意执行顺序与defer栈序,并优先选用成熟middleware框架。

Go 里没有 decorator 语法,但可以用高阶函数模拟
Go 语言本身不支持 Python 那种 @decorator 语法,也没有方法/函数级别的装饰器关键字。所谓“链式 decorator”,本质是**手动组合多个高阶函数**:每个装饰器接收一个 func(...) 并返回一个新的 func(...),再把它们一层层套用。
关键不是“怎么写装饰器”,而是“怎么让多层装饰器的参数、返回值、错误传递不脱节”。常见翻车点是:漏传参数、忽略原始函数的返回值类型、panic 没 recover、context 没透传。
例如,你想给一个 HTTP handler 加上日志 + 超时 + 认证,不能写成:
logDecorator(timeoutDecorator(authDecorator(h)))
而应该确保每个装饰器都严格保持签名一致(比如统一用 http.HandlerFunc),否则类型检查就过不去。
必须统一函数签名,否则链式调用直接编译失败
Go 是强类型语言,func(http.ResponseWriter, *http.Request) 和 func(context.Context, http.ResponseWriter, *http.Request) 是完全不同的类型,无法嵌套。所以所有装饰器必须约定好输入输出类型。
推荐做法是固定使用带 context.Context 的签名,哪怕暂时不用,也预留位置:
type HandlerFunc func(context.Context, http.ResponseWriter, *http.Request)
然后每个装饰器都实现为:
func WithLogger(next HandlerFunc) HandlerFunc { ... }
这样链式才安全:
final := WithRecovery(WithTimeout(WithLogger(myHandler)))
- 所有装饰器返回值类型必须和输入类型完全一致(包括参数顺序、指针/值语义)
- 不要在装饰器里擅自改变参数个数或类型,比如加一个
*sql.DB参数 —— 那就不是装饰器,是工厂函数 - 如果原始 handler 不带 context,先用
context.Background()包一层,别硬改签名
链式顺序影响执行逻辑,尤其涉及 defer / panic / cancel
装饰器链是“外层先执行,内层后执行”,类似洋葱模型。比如:
WithRecovery(WithTimeout(WithLogger(h)))
实际调用顺序是:Recovery → Timeout → Logger → h,但退出时是反向:h 返回 → Logger 收尾 → Timeout 判断是否超时 → Recovery 捕获 panic。
这就决定了:
-
WithTimeout必须在WithRecovery内层,否则 timeout 导致的 panic 可能被外层 recovery 拦截,掩盖真实超时问题 -
WithLogger如果在最外层,它记录的是整个链的起止;如果在中间,可能只记录到某一层就 panic 了,日志不完整 - 所有
defer语句按装饰器包裹顺序压栈,但执行是倒序,容易误判耗时归属
真正需要链式时,优先考虑 middleware 框架而非手写
自己拼接 WithA(WithB(WithC(h))) 看似灵活,但很快会遇到问题:
- 配置分散:每个装饰器都要单独传参(比如 logger 实例、timeout duration)
- 顺序难维护:加一个新装饰器就得重排整条链
- 测试困难:要 mock 中间某一层,得拆开整个链
更务实的做法是用成熟 middleware 模式,比如 Gin 的 engine.Use() 或 chi 的 router.Use(),它们内部已处理好类型对齐、context 透传、错误聚合。你自己写的“装饰器”只需专注单一职责,由框架负责组装。
手写链式装饰器只适合极简场景(比如 CLI 工具里包装几个纯函数),一旦涉及 HTTP、gRPC、数据库等 I/O,上下文、取消、超时、日志字段关联这些细节,靠裸函数链很难兜住。
最容易被忽略的一点:装饰器函数本身不应该持有状态(比如闭包捕获可变变量),否则并发调用时数据会错乱。所有依赖项(logger、db、config)必须通过参数传入,而不是在装饰器定义时 capture。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











