go装饰器是手动函数/接口包装,非自动拦截;常用func(context.context) error签名实现日志、超时等横切逻辑;叠加顺序敏感;http.handler中间件需显式调用next.servehttp;泛型优于反射;context cancel必须defer,否则泄漏。

Go 没有 @decorator 语法,所有“装饰器”都是手动包装函数或接口的显式组合,不是自动拦截,也不是运行时魔法——写错签名、漏调 next.ServeHTTP 或忘了 defer cancel(),它就直接失效。
func(context.Context) error 类型的高阶函数包装最常用
日志、超时、重试、限流这些横切逻辑,基本都围绕这个签名展开。它够通用,又能天然透传取消信号和超时控制。
- 必须保持输入输出类型完全一致,否则无法赋值给同名变量或传入期望该类型的参数位置
- 多个装饰器叠加时顺序敏感:
withTimeout(withRetry(f))表示先重试、再整体超时;反过来就变成每次重试都带独立 timeout - 别在闭包里捕获
panic后只打日志不 re-panic,否则上层收不到错误,HTTP handler 可能静默返回 200 - 示例:超时包装器要
defer cancel(),且原函数内部也得响应ctx.Done(),否则 goroutine 泄漏
http.Handler 中间件是接口级装饰的典型场景
Go 标准库的 func(http.Handler) http.Handler 模式,就是装饰器在接口层面的落地。
- 每个中间件接收一个
http.Handler,返回一个新的http.Handler,靠结构体嵌入 + 方法重写实现 - 关键点是包装体里必须显式调用
h.next.ServeHTTP(w, r),漏掉这句,请求就卡死在当前中间件 - 避免循环引用:不要在包装结构体里存自己的指针,比如
type authHandler struct { next *authHandler }是危险的 - 转换普通函数为
http.Handler时,要用http.HandlerFunc(f)显式转换,不能直接传
泛型包装器比反射更安全、更高效
有人想“一次写完适配所有函数签名”,于是用 reflect.MakeFunc。但真实项目里几乎没人这么干。
- 反射丢失编译期类型检查,IDE 跳不到原函数,
go vet和静态分析全失效 - 性能差一到两个数量级,高频调用(如每秒万级请求)下日志装饰器本身就能拖垮服务
- 真正可维护的做法是按常用签名写几个泛型版本,比如:
WithLog[T any](f func() (T, error))、WithLogCtx[T any](f func(context.Context) (T, error)) - 泛型方案既保类型安全,又免重复造轮子,还能让编译器帮你推导参数
context 传递和 cancel 管理最容易被忽略
几乎所有生产级装饰器都涉及 context,但很多人只记得传进去,却忘了它是个资源。
-
context.WithTimeout返回的cancel函数必须defer,否则超时前 panic 就泄漏 context - 限流装饰器里用
limiter.Wait(ctx),不能用context.Background(),否则上游 cancel 信号进不来 - 重试装饰器里,每次重试都要检查
ctx.Err(),否则三次 5 秒重试可能耗掉 15 秒,远超你设的 10 秒总 timeout - 不要在装饰器里自己起 goroutine 等 timeout,除非你能保证 cancel 后彻底清理所有子 goroutine
真正难的不是写出一个能跑的包装函数,而是让每个装饰器都正确处理 context 生命周期、错误分类、并发安全和资源释放——这些细节不会报编译错误,但上线后会在凌晨三点准时触发告警。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











