go-middleware
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo HTTP Middleware
Go HTTP 中间件
Quick Reference
快速参考
| Topic | Reference |
|---|---|
| Context keys, request IDs, user metadata | references/context-propagation.md |
| slog setup, logging middleware, child loggers | references/structured-logging.md |
| AppHandler pattern, domain errors, recovery | references/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 pattern. This is the composable building block for cross-cutting concerns in Go HTTP servers.
func(http.Handler) http.Handlergo
// 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 , return
http.Handler-- alwayshttp.Handler - Call to pass control to the next handler
next.ServeHTTP(w, r) - Work before the call (pre-processing) or after (post-processing) or both
- Use to propagate new context values downstream
r.WithContext(ctx)
所有中间件都遵循标准的模式,这是Go HTTP服务器中实现横切关注点的可组合构建块。
func(http.Handler) http.Handlergo
// 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 for request-scoped data that crosses API boundaries (request IDs, authenticated users, tenant IDs). Always use typed keys to avoid collisions.
context.WithValuego
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.WithValuego
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 (standard library, Go 1.21+) for structured logging in middleware. Wrap to capture the status code.
sloghttp.ResponseWritergo
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.
在中间件中使用(Go 1.21+ 标准库)实现结构化日志。包装以捕获响应状态码。
sloghttp.ResponseWritergo
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 so handlers don't need to write error responses themselves:
errorgo
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 function. Never leak internal error details to clients.
handleErrorSee references/error-handling-middleware.md for the full pattern with , , and JSON responses.
AppErrorerrors.As定义返回的自定义处理器类型,这样业务处理器无需自行编写错误响应:
errorgo
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)
}
}在统一的函数中,将领域错误映射为对应的HTTP状态码。切勿向客户端泄露内部错误细节。
handleError完整的实现模式(包含、和JSON响应)可参考references/error-handling-middleware.md。
AppErrorerrors.AsRecovery 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
推荐顺序
- Recovery -- outermost; catches panics from all inner middleware
- RequestID -- assign early so all subsequent middleware can reference it
- Logger -- logs the completed request with ID and status
- Auth -- after logging so failed auth attempts are recorded
- Application-specific middleware -- rate limiting, CORS, etc.
- Recovery -- 最外层;捕获所有内层中间件的Panic
- RequestID -- 尽早分配,以便后续所有中间件都能引用
- Logger -- 记录包含请求ID和状态码的完整请求日志
- Auth -- 在日志之后执行,确保认证失败的请求也能被记录
- 业务专属中间件 -- 限流、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会导致服务器崩溃。务必将恢复中间件放在最前面。