go-middleware

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go HTTP Middleware

Go HTTP 中间件

Quick Reference

快速参考

TopicReference
Context keys, request IDs, user metadatareferences/context-propagation.md
slog setup, logging middleware, child loggersreferences/structured-logging.md
AppHandler pattern, domain errors, recoveryreferences/error-handling-middleware.md
主题参考链接
上下文键、请求ID、用户元数据references/context-propagation.md
slog配置、日志中间件、子日志器references/structured-logging.md
AppHandler模式、领域错误、恢复机制references/error-handling-middleware.md

Middleware Signature

中间件签名

All middleware follows the standard
func(http.Handler) http.Handler
pattern. This is the composable building block for cross-cutting concerns in Go HTTP servers.
go
// Standard middleware signature
func RequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := r.Header.Get("X-Request-ID")
        if id == "" {
            id = uuid.New().String()
        }
        ctx := context.WithValue(r.Context(), requestIDKey, id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Type-safe context keys
type contextKey string
const requestIDKey contextKey = "request_id"

func RequestIDFromContext(ctx context.Context) string {
    id, _ := ctx.Value(requestIDKey).(string)
    return id
}
Key points:
  • Accept
    http.Handler
    , return
    http.Handler
    -- always
  • Call
    next.ServeHTTP(w, r)
    to pass control to the next handler
  • Work before the call (pre-processing) or after (post-processing) or both
  • Use
    r.WithContext(ctx)
    to propagate new context values downstream
所有中间件都遵循标准的
func(http.Handler) http.Handler
模式,这是Go HTTP服务器中实现横切关注点的可组合构建块。
go
// Standard middleware signature
func RequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := r.Header.Get("X-Request-ID")
        if id == "" {
            id = uuid.New().String()
        }
        ctx := context.WithValue(r.Context(), requestIDKey, id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Type-safe context keys
type contextKey string
const requestIDKey contextKey = "request_id"

func RequestIDFromContext(ctx context.Context) string {
    id, _ := ctx.Value(requestIDKey).(string)
    return id
}
核心要点:
  • 始终遵循接收
    http.Handler
    、返回
    http.Handler
    的规范
  • 调用
    next.ServeHTTP(w, r)
    将控制权传递给下一个处理器
  • 可在调用前后分别执行预处理、后处理逻辑,或两者兼具
  • 使用
    r.WithContext(ctx)
    向下游传递新的上下文值

Context Propagation

上下文传递

Use
context.WithValue
for request-scoped data that crosses API boundaries (request IDs, authenticated users, tenant IDs). Always use typed keys to avoid collisions.
go
type contextKey string

const (
    requestIDKey contextKey = "request_id"
    userKey      contextKey = "user"
)
Provide typed helper functions for extraction:
go
func RequestIDFromContext(ctx context.Context) string {
    id, _ := ctx.Value(requestIDKey).(string)
    return id
}
See references/context-propagation.md for user metadata patterns, downstream propagation, and timeouts.
对于跨API边界的请求作用域数据(如请求ID、已认证用户、租户ID),使用
context.WithValue
传递。务必使用类型化键以避免命名冲突。
go
type contextKey string

const (
    requestIDKey contextKey = "request_id"
    userKey      contextKey = "user"
)
提供类型化的提取辅助函数:
go
func RequestIDFromContext(ctx context.Context) string {
    id, _ := ctx.Value(requestIDKey).(string)
    return id
}
关于用户元数据模式、下游传递和超时设置,可参考references/context-propagation.md

Structured Logging

结构化日志

Use
slog
(standard library, Go 1.21+) for structured logging in middleware. Wrap
http.ResponseWriter
to capture the status code.
go
func Logger(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            wrapped := &statusWriter{ResponseWriter: w, status: http.StatusOK}

            next.ServeHTTP(wrapped, r)

            logger.Info("request completed",
                "method", r.Method,
                "path", r.URL.Path,
                "status", wrapped.status,
                "duration_ms", time.Since(start).Milliseconds(),
                "request_id", RequestIDFromContext(r.Context()),
            )
        })
    }
}
See references/structured-logging.md for JSON/text handler setup, log levels, and child loggers.
在中间件中使用
slog
(Go 1.21+ 标准库)实现结构化日志。包装
http.ResponseWriter
以捕获响应状态码。
go
func Logger(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            wrapped := &statusWriter{ResponseWriter: w, status: http.StatusOK}

            next.ServeHTTP(wrapped, r)

            logger.Info("request completed",
                "method", r.Method,
                "path", r.URL.Path,
                "status", wrapped.status,
                "duration_ms", time.Since(start).Milliseconds(),
                "request_id", RequestIDFromContext(r.Context()),
            )
        })
    }
}
关于JSON/文本处理器配置、日志级别和子日志器的内容,可参考references/structured-logging.md

Centralized Error Handling

集中式错误处理

Define a custom handler type that returns
error
so handlers don't need to write error responses themselves:
go
type AppHandler func(w http.ResponseWriter, r *http.Request) error

func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        handleError(w, r, err)
    }
}
Map domain errors to HTTP status codes in a single
handleError
function. Never leak internal error details to clients.
See references/error-handling-middleware.md for the full pattern with
AppError
,
errors.As
, and JSON responses.
定义返回
error
的自定义处理器类型,这样业务处理器无需自行编写错误响应:
go
type AppHandler func(w http.ResponseWriter, r *http.Request) error

func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        handleError(w, r, err)
    }
}
在统一的
handleError
函数中,将领域错误映射为对应的HTTP状态码。切勿向客户端泄露内部错误细节。
完整的实现模式(包含
AppError
errors.As
和JSON响应)可参考references/error-handling-middleware.md

Recovery Middleware

恢复中间件

Catch panics to prevent a single bad request from crashing the server:
go
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                slog.Error("panic recovered",
                    "panic", rec,
                    "stack", string(debug.Stack()),
                    "request_id", RequestIDFromContext(r.Context()),
                )
                writeJSON(w, 500, map[string]string{"error": "internal server error"})
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Recovery must be the outermost middleware so it catches panics from all inner middleware and handlers. See references/error-handling-middleware.md for details.
捕获Panic以防止单个异常请求导致服务器崩溃:
go
func Recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                slog.Error("panic recovered",
                    "panic", rec,
                    "stack", string(debug.Stack()),
                    "request_id", RequestIDFromContext(r.Context()),
                )
                writeJSON(w, 500, map[string]string{"error": "internal server error"})
            }
        }()
        next.ServeHTTP(w, r)
    })
}
恢复中间件必须是最外层的中间件,这样才能捕获所有内层中间件和处理器中的Panic。详细说明可参考references/error-handling-middleware.md

Middleware Chain Ordering

中间件链顺序

Apply middleware outermost-first. The first middleware in the chain wraps all others.
go
// Nested style (outermost first)
handler := Recovery(
    RequestID(
        Logger(
            Auth(
                router,
            ),
        ),
    ),
)

// Or with a chain helper
func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middleware) - 1; i >= 0; i-- {
        h = middleware[i](h)
    }
    return h
}

handler := Chain(router, Recovery, RequestID, Logger(slog.Default()), Auth)
中间件的应用顺序为从外到内,链中的第一个中间件会包裹所有后续中间件。
go
// 嵌套写法(最外层在前)
handler := Recovery(
    RequestID(
        Logger(
            Auth(
                router,
            ),
        ),
    ),
)

// 或使用链辅助函数
func Chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middleware) - 1; i >= 0; i-- {
        h = middleware[i](h)
    }
    return h
}

handler := Chain(router, Recovery, RequestID, Logger(slog.Default()), Auth)

Recommended Order

推荐顺序

  1. Recovery -- outermost; catches panics from all inner middleware
  2. RequestID -- assign early so all subsequent middleware can reference it
  3. Logger -- logs the completed request with ID and status
  4. Auth -- after logging so failed auth attempts are recorded
  5. Application-specific middleware -- rate limiting, CORS, etc.
  1. Recovery -- 最外层;捕获所有内层中间件的Panic
  2. RequestID -- 尽早分配,以便后续所有中间件都能引用
  3. Logger -- 记录包含请求ID和状态码的完整请求日志
  4. Auth -- 在日志之后执行,确保认证失败的请求也能被记录
  5. 业务专属中间件 -- 限流、CORS等

Anti-patterns

反模式

Using string or int context keys

使用字符串或整数作为上下文键

go
// BAD: collisions with other packages
ctx = context.WithValue(ctx, "user", user)

// GOOD: unexported typed key
type contextKey string
const userKey contextKey = "user"
ctx = context.WithValue(ctx, userKey, user)
go
// 错误:可能与其他包发生命名冲突
ctx = context.WithValue(ctx, "user", user)

// 正确:使用未导出的类型化键
type contextKey string
const userKey contextKey = "user"
ctx = context.WithValue(ctx, userKey, user)

Writing response before calling next

在调用next之前写入响应

go
// BAD: writes response then continues chain
func Bad(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK) // too early!
        next.ServeHTTP(w, r)
    })
}
go
// 错误:先写入响应再继续执行链
func Bad(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK) // 时机过早!
        next.ServeHTTP(w, r)
    })
}

Forgetting to call next.ServeHTTP

忘记调用next.ServeHTTP

go
// BAD: swallows the request
func Bad(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("got request")
        // forgot next.ServeHTTP(w, r)
    })
}
go
// 错误:请求被吞噬
func Bad(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("got request")
        // 遗漏了next.ServeHTTP(w, r)
    })
}

Storing large objects in context

在上下文中存储大对象

Context values should be small, request-scoped metadata (IDs, tokens, user structs). Never store database connections, file handles, or large payloads.
上下文值应是轻量级的请求作用域元数据(ID、令牌、用户结构体等)。切勿存储数据库连接、文件句柄或大型负载。

Using context.WithValue for function parameters

使用context.WithValue传递函数参数

If a function needs a value to do its job, pass it as an explicit parameter. Context is for cross-cutting metadata that passes through APIs, not for avoiding function signatures.
如果函数需要某个值才能完成工作,应将其作为显式参数传递。上下文仅用于跨API传递的横切元数据,而非简化函数签名的手段。

Recovery middleware in the wrong position

恢复中间件位置错误

If recovery is not the outermost middleware, panics in outer middleware will crash the server. Always apply recovery first.
如果恢复中间件不是最外层,外层中间件中的Panic会导致服务器崩溃。务必将恢复中间件放在最前面。