Go 不支持装饰器语法,所有中间件逻辑必须显式实现;应使用 func(http.Handler) http.Handler 签名、注意调用顺序与 nil 检查,并优先采用结构体封装以保障并发安全与可测试性。

Go 里没有装饰器,别被 Python 或 JS 习惯带偏
Go 语言本身不支持函数装饰器(decorator)语法,@auth、@cache 这类写法在 Go 中根本无法编译。这不是“还没实现”,而是语言设计上明确拒绝高阶语法糖——所有逻辑必须显式表达。
常见错误现象:undefined: auth 或 syntax error: unexpected @,往往是因为开发者直接把 Python Flask/JS Express 的中间件写法照搬进 Go 文件。
- Go 的函数是一等公民,但没有运行时元编程能力,无法自动包裹函数
- 所谓“装饰器效果”,只能靠手动组合函数(如
http.HandlerFunc链式调用)或结构体方法包装实现 - 第三方库(如
alice、negroni)提供的With()看似像装饰器,本质仍是显式构造中间件链,不是语法层特性
HTTP 中间件的正确写法:用闭包封装 handler
标准库 http.Handler 接口只接受一个 ServeHTTP(http.ResponseWriter, *http.Request) 方法,中间件必须返回新 http.Handler 或 http.HandlerFunc。
典型错误:试图在 handler 内部“注入”逻辑而不调用 next.ServeHTTP(),导致请求卡死或 404。
立即学习“go语言免费学习笔记(深入)”;
- 中间件函数签名应为:
func(http.Handler) http.Handler,不是func(http.HandlerFunc) http.HandlerFunc(后者容易漏掉http.ResponseWriter的包装) - 务必检查
next是否为nil,尤其在嵌套多层时,避免 panic:panic: runtime error: invalid memory address or nil pointer dereference - 日志、认证、CORS 等通用逻辑,优先用
http.Handler实现,而非闭包捕获变量——便于单元测试和复用
示例(带错误防护):
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-API-Key") == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r) // 必须显式调用
})
}
中间件顺序决定行为,顺序错就白写
Go Web 中间件是线性执行的,前一个中间件不调用 next.ServeHTTP(),后续全部中断。这和 Express 的 next() 或 Django 的 middleware 顺序一致,但 Go 没有框架自动管理,全靠手排。
常见错误现象:CORS 头没生效、JWT 解析后用户信息丢失、日志里看不到 POST body —— 往往是中间件位置放错了。
-
Recovery(panic 捕获)必须放在最外层,否则内部 panic 会直接崩溃进程 -
Logging放最外层能看到完整耗时;放内层可能因前面中间件提前返回而漏记 -
Body reading类中间件(如解析 JSON)必须在依赖 body 的中间件之前,且只能读一次 ——r.Body是io.ReadCloser,读完即空 - 使用
chi或gin时,注意它们的Use()/Group().Use()是按注册顺序入栈,不是按调用顺序
结构体 + 方法比闭包更可控,尤其涉及状态时
当需要共享配置(如 Redis client、DB 连接池)或维护状态(如计数器、缓存 map),用闭包捕获变量容易引发 goroutine 安全问题或内存泄漏。
错误做法:在闭包里直接用 var cache = map[string]string{},多个请求并发写入会 panic。
- 把中间件逻辑封装成结构体,字段存依赖项,方法实现
http.Handler接口,天然支持依赖注入 - 共享资源(如 sync.Map、*redis.Client)作为结构体字段传入,避免全局变量
- 初始化时做参数校验(如检查
timeout < 0),比运行时 panic 更早暴露问题
示例(带超时控制):
type TimeoutMiddleware struct {
timeout time.Duration
}
func (m TimeoutMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), m.timeout)
defer cancel()
r = r.WithContext(ctx)
ch := make(chan struct{})
go func() {
http.DefaultServeMux.ServeHTTP(w, r)
close(ch)
}()
select {
case <-ch:
case <-ctx.Done():
http.Error(w, "Request timeout", http.StatusGatewayTimeout)
}
}
真正难的不是写一个中间件,而是让十几个中间件在不同环境(dev/staging/prod)、不同路由组、不同子服务中保持行为一致。配置来源、错误传播方式、context 传递深度,这些细节比语法选择重要得多。










