Go HTTP中间件本质是func(http.Handler)http.Handler函数链,通过闭包组合处理器,需显式调用如loggingMiddleware(authMiddleware(handler)),顺序决定执行时机,拦截响应需return避免重复写header,数据传递依赖context.Context。

Go HTTP 中间件的本质是函数链式调用
Go 标准库的 http.Handler 接口只要求实现 ServeHTTP(http.ResponseWriter, *http.Request) 方法,中间件就是满足该签名、且内部调用下一个处理器的函数。它不是框架概念,而是基于闭包和高阶函数的自然表达。
常见错误是试图“注册中间件”后自动生效——Go 没有全局中间件注册表,必须显式拼接调用链。比如 loggingMiddleware(authMiddleware(handler)) 才是正确写法,而不是期待某个 Use() 方法自动注入。
- 中间件函数本身不处理请求,只做预处理/后处理,并决定是否调用
next.ServeHTTP(w, r) - 顺序很重要:越靠外的中间件越早执行(请求进入时),也越晚执行(响应返回时)
- 不要在中间件里直接 write header 或 body 后还调用
next.ServeHTTP,会导致http: multiple response.WriteHeader calls错误
用 func(http.Handler) http.Handler 构建可复用中间件
这是最主流、最符合 Go 风格的中间件签名。它接收一个 http.Handler,返回一个新的 http.Handler,便于组合与测试。
例如日志中间件:
立即学习“go语言免费学习笔记(深入)”;
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("START %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("END %s %s", r.Method, r.URL.Path)
})
}
使用时链式包裹:
handler := http.HandlerFunc(yourHandler)
handler = loggingMiddleware(handler)
handler = authMiddleware(handler)
http.ListenAndServe(":8080", handler)
- 所有中间件都应接受
http.Handler而非http.HandlerFunc,否则无法兼容自定义 struct 实现的 Handler - 若需传参(如白名单路径、密钥),用闭包捕获:
func(jwtKey string) func(http.Handler) http.Handler { ... } - 注意
http.HandlerFunc是类型别名,不是接口;它只是让函数能转成http.Handler的快捷方式
拦截请求并提前终止响应的关键点
中间件拦截请求的核心动作是:不调用 next.ServeHTTP(w, r),而是直接向 w 写入响应(status code + body)。
典型场景包括鉴权失败、限流超限、路径非法等:
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return // ← 必须 return,否则继续执行 next
}
// 验证 token...
if !isValid(token) {
http.Error(w, "Invalid token", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
- 调用
http.Error或w.WriteHeader+w.Write后,必须return,否则后续逻辑可能 panic 或写出重复 header - 不要在中间件里修改
*http.Request字段(如r.URL.Path)后直接传给 next——它不可变;如需改写路径,应构造新*http.Request(用r.Clone(r.Context())) - 如果中间件需要读取 request body(如解析 JSON),记得用
r.Body并defer r.Body.Close(),否则下游 handler 会读到空内容
中间件链中传递数据用 context.Context
Go 不支持类似 Express 的 res.locals 或 Django 的 request attributes,跨中间件共享数据唯一正统方式是通过 r.Context()。
例如把用户 ID 注入 context 供下游 handler 使用:
func userContextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := extractUserID(r) // 从 token 或 cookie 解析
ctx := context.WithValue(r.Context(), "user_id", userID)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
// 在最终 handler 中获取:
func yourHandler(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value("user_id").(string) // 注意类型断言
// ...
}
- 键建议用私有类型避免冲突:
type ctxKey string; const userIDKey ctxKey = "user_id" - 不要用字符串字面量作 key,极易因拼写错误导致 nil panic
- context 值仅存活于单次请求生命周期,无需担心并发污染










