Go里没有装饰器语法,需用高阶函数模拟:接收原函数并返回增强后的新函数,签名必须严格一致,HTTP中间件是典型应用,执行顺序由嵌套层次决定,context透传和性能开销是关键注意事项。

Go 里没有装饰器语法,但可以用函数值模拟
Go 语言本身不支持 Python 那种 @decorator 语法,也没有方法重写或动态代理机制。所谓“装饰器模式”,本质是用高阶函数封装行为:把原函数作为参数传入,返回一个增强后的新函数。关键不是语法糖,而是控制逻辑注入的时机和方式。
常见错误是试图用 struct 方法“装饰”另一个方法,结果发现无法覆盖调用链、或闭包捕获了错误的 receiver 实例。正确做法始终围绕 func(...) 类型展开。
- 装饰目标必须是函数类型,比如
func(context.Context, *Request) (*Response, error) - 装饰器本身也得是函数,接收原函数并返回新函数,签名需严格一致
- 不要在装饰器内部直接调用原函数——要返回一个函数,等被调用时才执行
用中间件风格实现 HTTP 请求日志切面
这是最典型的切面场景:在 handler 执行前后加日志、耗时统计、鉴权等。Go 的 http.Handler 接口天然适合链式装饰,http.HandlerFunc 可转为函数值参与组合。
func WithLogging(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("START %s %s", r.Method, r.URL.Path)
start := time.Now()
next(w, r)
log.Printf("END %s %s (%v)", r.Method, r.URL.Path, time.Since(start))
}
}
注意点:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须用
next(w, r)而非next.ServeHTTP(w, r)—— 因为next是http.HandlerFunc类型,已具备ServeHTTP方法,但直接调用函数更符合装饰器语义 - 如果 handler 会修改
ResponseWriter(如写 header),装饰器需确保不干扰原始行为,避免提前 write 导致 panic - 多个装饰器嵌套时,顺序决定执行流:最外层先执行,最内层最后执行
带上下文传递的通用装饰器模板
真实业务中常需跨装饰器传递数据(如用户 ID、trace ID),靠全局变量或额外参数都不够干净。标准解法是用 context.Context 作为载体,在装饰器中 context.WithValue 注入,并在下游 handler 中取用。
func WithAuth(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
userID, err := parseToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), "userID", userID)
// 注意:必须用 r.WithContext() 构造新 *http.Request
next(w, r.WithContext(ctx))
}
}
容易踩的坑:
-
r.Context()是只读副本,WithValue不会修改原 request,必须显式调用r.WithContext() - 键名建议用自定义类型(如
type ctxKey string)避免字符串冲突,而不是裸用"userID" - 不要在装饰器里调用
http.Error后还继续执行next—— 这会导致双写 response 报错
性能与调试陷阱:别让装饰器拖慢关键路径
装饰器看似轻量,但每次 HTTP 请求都会新建闭包、分配 context、触发多次函数调用。高频接口上,几层装饰器叠加可能带来可观开销。
实测过的情况:
- 连续 5 层装饰器 +
context.WithValue,相比裸 handler,QPS 下降约 8%~12%(Go 1.21,小对象场景) - 日志装饰器若用
log.Printf而非预格式化字符串,会触发反射和内存分配,比fmt.Sprintf慢 3 倍以上 - panic 恢复装饰器(
defer/recover)必须放在最外层,否则内层 panic 会跳过中间装饰器的 defer
真正难的是平衡可维护性与性能:该抽成装饰器的逻辑,和该硬编码进 handler 的逻辑之间,边界往往模糊。比如一个只在某几个 endpoint 用的权限检查,强行塞进通用装饰器反而增加心智负担。


















