关于 go-middleware
go-middleware 是面向 Go HTTP 服务开发者的中间件实践技能,涵盖请求上下文、结构化日志与集中式错误处理,并说明中间件链的顺序和审查要点。
主要功能
- 01标准 HTTP 中间件签名
- 02请求 ID 与上下文传递
- 03slog 结构化日志
- 04集中式错误处理与 panic 恢复
- 05中间件链顺序检查
适用场景
- 01为请求传递 ID、用户或租户信息
- 02记录请求状态并生成结构化日志
- 03统一映射领域错误为 HTTP 响应
- 04检查中间件链中的恢复与转发逻辑
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ existential-birds/ beagle --skill go-middleware安装后在 Claude Code 中输入/go-middleware确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/context-propagation.mderror-handling-middleware.mdstructured-logging.md详细介绍
Go HTTP 中间件
快速参考
| 主题 | 参考 |
|---|---|
| 上下文键、请求 ID、用户元数据 | references/context-propagation.md |
| slog 设置、日志中间件、子日志记录器 | references/structured-logging.md |
| AppHandler 模式、领域错误、恢复 | references/error-handling-middleware.md |
中间件签名
所有中间件都遵循标准的 func(http.Handler) http.Handler 模式。这是 Go HTTP 服务中处理横切关注点的可组合构建块。
// 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)将新的上下文值向下游传递
上下文传递
对于跨越 API 边界的请求范围数据(请求 ID、经过身份验证的用户、租户 ID),请使用 context.WithValue。始终使用有类型的键,以避免冲突。
type contextKey string
const (
requestIDKey contextKey = "request_id"
userKey contextKey = "user"
)
为提取数据提供有类型的辅助函数:
func RequestIDFromContext(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}
有关用户元数据模式、下游传递和超时,请参阅 references/context-propagation.md。
结构化日志记录
在中间件中使用 slog(Go 1.21+ 标准库)进行结构化日志记录。封装 http.ResponseWriter 以捕获状态码。
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。
集中式错误处理
定义一个返回 error 的自定义处理器类型,这样处理器就无需自行编写错误响应:
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。
恢复中间件
捕获 panic,防止单个错误请求导致服务崩溃:
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。
中间件链顺序
按从最外层到最内层的顺序应用中间件。链中的第一个中间件会封装其余所有中间件。
// 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)
建议顺序
- Recovery——位于最外层;捕获所有内部中间件中的 panic
- RequestID——尽早分配,以便所有后续中间件都能引用它
- Logger——记录包含 ID 和状态的已完成请求
- Auth——置于日志记录之后,以记录身份验证失败的尝试
- 特定于应用的中间件——限流、CORS 等
合并或审查前的检查关卡
按以下顺序进行客观的通过/不通过检查;不要用“我已经在脑中验证过了”来代替。
- Recovery 的位置
- 找出服务器构建中间件链的位置(例如
main、路由器的Use,或Chain辅助函数)。 - 通过: Recovery 按照中间件链顺序包装所有其他中间件和最终处理程序(嵌套风格中位于最外层,或辅助函数中的
Chain参数顺序正确)。注明文件路径和完整的链代码片段。
- 找出服务器构建中间件链的位置(例如
- 感知状态的中间件使用包装后的
ResponseWriter- 如果中间件在处理程序运行后记录或记录 HTTP 状态,则必须将包装后的 writer 传入
next.ServeHTTP,而不能只传入原始 writer。 - 通过: 代码片段显示,在
next返回后观察状态时,使用next.ServeHTTP(wrapped, r)(或等效写法)。
- 如果中间件在处理程序运行后记录或记录 HTTP 状态,则必须将包装后的 writer 传入
- 每条继续处理的路径都会调用
next- 检查每个中间件的控制流。
- 通过: 除非某条分支有意发送响应(例如身份验证失败),否则没有任何分支会丢弃请求而不调用
next.ServeHTTP;这些提前终止的情况在代码审查中应一目了然。
反模式
使用字符串或整数作为上下文键
// 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)
调用 next 之前写入响应
// 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)
})
}
忘记调用 next.ServeHTTP
// 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)
})
}
在上下文中存储大型对象
上下文值应为较小的、限定于请求范围的元数据(ID、令牌、用户结构体)。切勿存储数据库连接、文件句柄或大型负载。
使用 context.WithValue 传递函数参数
如果函数需要某个值才能完成工作,请将其作为显式参数传入。上下文用于在 API 中传递横切元数据,而不是用于规避函数签名。
Recovery 中间件位置错误
如果 Recovery 不是最外层中间件,外层中间件中的 panic 会导致服务器崩溃。始终最先应用 Recovery。