go 中用函数类型实现装饰器需定义函数类型别名(如 type handler func(string) error),装饰器接收并返回同类型函数,通过闭包包装调用逻辑;http 场景应使用 middleware func(http.handler) http.handler 标准模式。

Go 里怎么用函数类型实现装饰器
Go 没有 Python 那种 @decorator 语法,但靠函数类型和闭包能干净地复现装饰器语义——核心是让一个函数接收另一个函数作为参数,并返回新函数。
典型模式:func(fn T) T 类型的装饰器,其中 T 是具体函数签名(比如 func(int) string)。它不修改原函数,只包装调用逻辑。
- 必须显式声明函数类型别名,否则嵌套写法极难读,例如:
type Handler func(string) error,再定义func Logging(h Handler) Handler - 装饰器内部调用原函数时,要确保传参、返回值完全一致;漏传
...args或错接返回值会导致编译失败或 panic - 不要在装饰器里直接调用原函数并返回结果(即
return h(x)),这会丢失“装饰”能力;正确做法是返回一个新函数:return func(x string) error { ... h(x) ... }
为什么 log 装饰器常在 defer 里失效
装饰器里加日志,如果用 defer fmt.Println("done"),大概率看不到输出——因为 defer 绑定的是装饰器函数体的执行上下文,不是被包装函数的。
真实场景:你希望每次 HTTP handler 执行完才打日志,但装饰器函数本身瞬间返回,defer 立刻触发,而非等 handler 执行完。
立即学习“go语言免费学习笔记(深入)”;
- 正确做法是在返回的闭包里放
defer:return func(x string) error { defer log.Printf("end: %s", x); return h(x) } - 如果需要记录耗时,必须用
time.Now()在闭包开头打点,不能在装饰器外层打点 - 注意闭包捕获变量:若装饰器参数是
func() error,而你在闭包里用了外部循环变量(如for _, v := range xs { decs = append(decs, Decorate(v)) }),所有闭包会共享最后一个v的值
多个装饰器串联时顺序为什么重要
Go 中没有自动链式调用机制,装饰器必须手动嵌套:Auth(Logging(Handler))。顺序决定执行流,也决定错误传播路径和中间状态可见性。
常见错误现象:Logging(Auth(Handler)) 导致日志里永远看不到认证失败的请求——因为 Auth 在最内层,未通过就直接返回错误,Logging 的闭包根本没执行。
- 一般原则:越靠近业务逻辑的装饰器越靠内(如
Handler),越通用/越早介入的越靠外(如Recovery、Timeout) - 如果装饰器有副作用(如修改 context、写 header),顺序错会导致 header 已发送后还尝试写,触发
http: multiple response.WriteHeader calls - 避免深度嵌套:超过 3 层建议用辅助函数组装,比如
Chain(Auth, Logging, Recovery)(h),但注意Chain内部仍需按从外到内的顺序应用
HTTP handler 装饰器里怎么安全取 context 和 request
标准 http.Handler 接口是 func(http.ResponseWriter, *http.Request),没法直接塞进 func(string) error 这类简单签名。硬转类型会丢信息,也破坏可测试性。
真正可用的方式是定义专用 handler 装饰器类型:type Middleware func(http.Handler) http.Handler,这是 Go 生态事实标准(如 gorilla/mux、chi 都用它)。
- 不要试图把
*http.Request拆成字段传给泛型装饰器;request.Body 是 io.ReadCloser,只能读一次,提前读会导致下游 handler 读不到 - 如果要在装饰器里读 body,必须用
io.ReadAll后重新构造io.NopCloser放回 request,否则后续 handler 会卡住 - context 传递必须用
req = req.WithContext(...)显式替换,原 request.Context() 是只读副本,改了也没用
return 位置,或少一次 req.WithContext,问题就藏得又深又静。










