go中无装饰器语法,需用高阶函数组合实现:以handler为参数返回新handler,通过withrecovery、withlogging等函数链式调用,注意顺序、context透传、body不可重复读及避免过度嵌套。

Go 里没有装饰器语法,但可以用函数值组合模拟
Go 语言本身不支持 Python 那种 @decorator 语法,也没法在运行时动态“包装”方法签名。所谓“装饰器模式”,在 Go 里本质是**函数式组合**:把一个 func(...) 当作参数传给另一个函数,返回新的 func(...),中间插入日志、重试、超时等逻辑。
关键不是模仿语法,而是复用行为。比如你有一组处理 HTTP 请求的 handler:http.HandlerFunc,想统一加日志和 panic 捕获,就写个函数接收原 handler,返回新 handler。
- 不要试图用 struct 方法 + 接口 + 匿名字段“硬套” OOP 装饰器——容易绕晕且丧失 Go 的简洁性
- 优先选高阶函数(higher-order function),而不是定义一堆
Decorator接口和实现 - 注意闭包捕获变量的生命周期:如果装饰器里保存了 request 或 context,别让它意外逃逸到 goroutine 外
用 http.HandlerFunc 实现无侵入日志+错误恢复
这是最典型、也最容易踩坑的场景。很多人直接在 handler 内部写 log.Printf,结果日志位置分散、无法统一控制格式或采样率。正确做法是抽成可复用的装饰函数:
func WithRecovery(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic in %s %s: %v", r.Method, r.URL.Path, err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next(w, r)
}
}
func WithLogging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
}
}
使用时链式调用:http.HandleFunc("/api/user", WithLogging(WithRecovery(userHandler)))。顺序很重要:WithRecovery 必须在最内层,否则外层 panic 不会被捕获。
立即学习“go语言免费学习笔记(深入)”;
- 别在
WithLogging里读取r.Body或调用r.ParseForm()——会消耗 body,导致下游 handler 读不到数据 - 如果需要记录请求体或响应体,得用
io.TeeReader或自定义ResponseWriter包装,不是简单加个 log 就行 - 所有装饰器函数必须接收并调用
next,漏掉这句就等于静默丢弃请求
Context 传递必须显式透传,不能依赖全局或闭包
Go 的 context.Context 是取消、超时、请求范围值的载体,但它不会自动穿透装饰器链。常见错误是:在装饰器里创建新 context.WithTimeout,却没把它传给 next;或者把 context.Value 存在闭包里,误以为下游能访问。
正确方式是让 handler 签名支持 context.Context,或用标准 http.Handler 接口配合 r.Context():
func WithTimeout(d time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), d)
defer cancel()
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
- 永远用
r.WithContext()替换 request,而不是改写闭包里的变量 - 别在装饰器里调用
ctx.Done()后还继续执行业务逻辑——超时后应立即返回 - 如果 handler 内部启动 goroutine,必须把
ctx显式传进去,并监听ctx.Done()
性能敏感场景下避免过度装饰器嵌套
每个装饰器都是一次函数调用 + 一次闭包创建。10 层嵌套不是语法错误,但会让调用栈变深、GC 压力上升、pprof 分析变模糊。真实服务中见过因 7 层装饰器导致 15% CPU 耗在 runtime.call64 上的情况。
判断是否“过度”的信号:handler 函数体比装饰器逻辑还短;同一请求路径上重复应用相同装饰器(比如两个 WithLogging);装饰器内部做同步 I/O(如查 Redis)却没加缓存。
- 把高频共用逻辑(如 auth 校验)提前到中间件链前端,失败直接 return,避免后续装饰器空转
- 用 benchmark 对比:单层 vs 五层装饰器的
BenchmarkHandler,看 allocs/op 是否翻倍 - 如果某装饰器只对特定 path 生效(比如 /admin/ 才鉴权),别全局注册,用路由分组或条件分支更轻量
真正难的不是写出五个嵌套的 WithXxx,而是想清楚哪一层该 abort,哪一层该 fallback,哪一层其实根本不需要独立成装饰器——它只是 handler 里三行 if 语句的事。










