3. 标准库中间件实现:使用 http.HandlerFunc 编写日志、鉴权、超时中间件
在 Go 微服务中,中间件是构建可观测性、安全性和可靠性的基石。Go 标准库 net/http 提供了 http.Handler 接口,通过 http.HandlerFunc 适配器,我们可以用函数式的方式轻松实现中间件链。本章将手把手带你实现三个最核心的中间件:日志、鉴权 和 超时控制。
核心思想: 中间件是一个接收
http.Handler 并返回 http.Handler 的函数。通过嵌套调用,形成请求处理链。
3.1 中间件模式与链式结构
标准库中间件的本质是 装饰器模式。每个中间件包裹下一个 http.Handler,在请求到达最终处理器之前或之后执行横切逻辑。
3.2 日志中间件:请求可观测性
日志中间件记录每个请求的 方法、路径、耗时 和 状态码。这是微服务可观测性的第一道防线。
// loggingMiddleware 记录请求日志
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 包装 ResponseWriter 以捕获状态码
lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
// 调用下一个处理器
next.ServeHTTP(lrw, r)
// 后处理:记录日志
duration := time.Since(start)
log.Printf("[%s] %s %d %v",
r.Method,
r.URL.Path,
lrw.statusCode,
duration,
)
})
}
// loggingResponseWriter 包装 http.ResponseWriter 以捕获状态码
type loggingResponseWriter struct {
http.ResponseWriter
statusCode int
}
func (lrw *loggingResponseWriter) WriteHeader(code int) {
lrw.statusCode = code
lrw.ResponseWriter.WriteHeader(code)
}
关键点: 通过自定义
loggingResponseWriter 包装标准 http.ResponseWriter,我们可以在不破坏接口的前提下捕获 WriteHeader 写入的状态码。
3.3 鉴权中间件:安全准入控制
鉴权中间件从请求头中提取 Token,验证其有效性。若验证失败,直接返回 401 Unauthorized,阻止请求继续向下传递。
// authMiddleware 简单的 Bearer Token 鉴权
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 从 Authorization 头提取 Token
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing Authorization header", http.StatusUnauthorized)
return
}
// 期望格式: "Bearer <token>"
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid Authorization format", http.StatusUnauthorized)
return
}
token := parts[1]
// 验证 Token(此处为示例,实际应调用认证服务或 JWT 校验)
if !validateToken(token) {
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
return
}
// 将用户信息注入上下文(可选)
ctx := context.WithValue(r.Context(), "user", "demo_user")
r = r.WithContext(ctx)
// 鉴权通过,调用下一个处理器
next.ServeHTTP(w, r)
})
}
// validateToken 模拟 Token 校验
func validateToken(token string) bool {
// 实际项目中应使用 JWT 库或调用认证服务
return token == "valid-token-123"
}
安全提醒: 生产环境切勿硬编码 Token 或使用简单字符串比较。应使用
github.com/golang-jwt/jwt/v5 等标准库进行 JWT 签名验证,或集成 OAuth2/OIDC 协议。
3.4 超时中间件:防止资源耗尽
超时中间件使用 context.WithTimeout 控制请求的最大执行时间。当超时发生时,自动取消上下文并返回 503 Service Unavailable,避免 goroutine 泄漏。
// timeoutMiddleware 设置请求超时
func timeoutMiddleware(timeout 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(), timeout)
defer cancel()
// 替换请求的上下文
r = r.WithContext(ctx)
// 使用 channel 同步处理结果
done := make(chan struct{})
go func() {
next.ServeHTTP(w, r)
close(done)
}()
// 等待处理完成或超时
select {
case <-done:
// 正常完成
case <-ctx.Done():
// 超时
http.Error(w, "Request timeout", http.StatusServiceUnavailable)
}
})
}
}
最佳实践: 超时时间应根据业务场景设置,通常建议 5s-30s。对于数据库查询或外部 API 调用,应设置更短的超时(如 2s-5s),并在业务代码中主动检查
ctx.Done()。
3.5 中间件组合与路由挂载
将三个中间件组合成链,并挂载到具体的路由上。注意中间件的顺序:日志 → 鉴权 → 超时 → 业务处理器。
func main() {
// 最终业务处理器
helloHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 模拟业务处理耗时
time.Sleep(100 * time.Millisecond)
w.WriteHeader(http.StatusOK)
w.Write([]byte("Hello, Microservice!"))
})
// 构建中间件链(从外到内:日志 → 鉴权 → 超时)
handler := loggingMiddleware(
authMiddleware(
timeoutMiddleware(5 * time.Second)(helloHandler),
),
)
// 注册路由
http.Handle("/api/hello", handler)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
3.6 三种中间件对比
| 中间件 | 职责 | 核心机制 | 失败处理 | 性能影响 |
|---|---|---|---|---|
| 日志 | 记录请求元数据与耗时 | 包装 ResponseWriter + time.Since | 不影响业务,仅记录 | 极低(纳秒级) |
| 鉴权 | 验证请求身份 | 提取 Token + 校验 + 上下文注入 | 返回 401,阻断链 | 低(取决于校验算法) |
| 超时 | 控制请求最大执行时间 | context.WithTimeout + select | 返回 503,取消上下文 | 低(goroutine 切换开销) |
3.7 常见陷阱与避坑指南
- ResponseWriter 包装顺序: 日志中间件必须包装在鉴权外层,否则无法记录被鉴权拒绝的请求。
- 超时后 goroutine 泄漏: 超时中间件中,即使
ctx.Done()触发,启动的 goroutine 仍会继续运行直到next.ServeHTTP返回。务必在业务代码中监听ctx.Done()并提前退出。 - 上下文传递: 使用
r.WithContext(ctx)替换请求上下文后,后续中间件和处理器应通过r.Context()获取,而非直接使用context.TODO()。 - panic 传播: 标准中间件不捕获 panic。如需恢复,应额外添加
recoverMiddleware作为最外层中间件。
3.8 本章小结
通过标准库 http.HandlerFunc,我们实现了三个生产级中间件:
- 日志中间件 通过包装 ResponseWriter 捕获状态码和耗时。
- 鉴权中间件 通过提取并校验 Token 实现安全准入,失败时阻断链。
- 超时中间件 通过
context.WithTimeout和select控制请求生命周期。