3. 标准库中间件实现:使用 http.HandlerFunc 编写日志、鉴权、超时中间件

在 Go 微服务中,中间件是构建可观测性、安全性和可靠性的基石。Go 标准库 net/http 提供了 http.Handler 接口,通过 http.HandlerFunc 适配器,我们可以用函数式的方式轻松实现中间件链。本章将手把手带你实现三个最核心的中间件:日志、鉴权 和 超时控制。

核心思想: 中间件是一个接收 http.Handler 并返回 http.Handler 的函数。通过嵌套调用,形成请求处理链。

3.1 中间件模式与链式结构

标准库中间件的本质是 装饰器模式。每个中间件包裹下一个 http.Handler,在请求到达最终处理器之前或之后执行横切逻辑。

标准库中间件链式调用流程 中间件链式调用流程 请求 日志中间件 记录请求/响应 鉴权中间件 校验 Token 超时中间件 控制执行时长 业务处理 响应沿原路返回,每个中间件可后处理 中间件顺序决定执行与返回的先后

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 作为最外层中间件。
中间件组合与数据流 中间件组合与数据流 请求 日志 记录请求 包装 ResponseWriter 鉴权 提取 Token 校验 + 注入 ctx 超时 context.WithTimeout select 多路复用 业务处理 响应沿原路返回,日志中间件最后记录状态码 鉴权失败 → 直接返回 401

3.8 本章小结

通过标准库 http.HandlerFunc,我们实现了三个生产级中间件:

  • 日志中间件 通过包装 ResponseWriter 捕获状态码和耗时。
  • 鉴权中间件 通过提取并校验 Token 实现安全准入,失败时阻断链。
  • 超时中间件 通过 context.WithTimeout 和 select 控制请求生命周期。