mcpField
G
go-middleware

go-middleware

github.com/existential-birds开发8589 次安装11 周前更新
代码仓库

关于 go-middleware

go-middleware 是面向 Go HTTP 服务开发者的中间件实践技能,涵盖请求上下文、结构化日志与集中式错误处理,并说明中间件链的顺序和审查要点。

主要功能

  1. 01标准 HTTP 中间件签名
  2. 02请求 ID 与上下文传递
  3. 03slog 结构化日志
  4. 04集中式错误处理与 panic 恢复
  5. 05中间件链顺序检查

适用场景

  1. 01为请求传递 ID、用户或租户信息
  2. 02记录请求状态并生成结构化日志
  3. 03统一映射领域错误为 HTTP 响应
  4. 04检查中间件链中的恢复与转发逻辑

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill go-middleware

安装后在 Claude Code 中输入/go-middleware确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
context-propagation.md
error-handling-middleware.md
structured-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)
建议顺序
  1. Recovery——位于最外层;捕获所有内部中间件中的 panic
  2. RequestID——尽早分配,以便所有后续中间件都能引用它
  3. Logger——记录包含 ID 和状态的已完成请求
  4. Auth——置于日志记录之后,以记录身份验证失败的尝试
  5. 特定于应用的中间件——限流、CORS 等

合并或审查前的检查关卡

按以下顺序进行客观的通过/不通过检查;不要用“我已经在脑中验证过了”来代替。

  1. Recovery 的位置
    • 找出服务器构建中间件链的位置(例如 main、路由器的 Use,或 Chain 辅助函数)。
    • 通过: Recovery 按照中间件链顺序包装所有其他中间件和最终处理程序(嵌套风格中位于最外层,或辅助函数中的 Chain 参数顺序正确)。注明文件路径和完整的链代码片段。
  2. 感知状态的中间件使用包装后的 ResponseWriter
    • 如果中间件在处理程序运行后记录或记录 HTTP 状态,则必须将包装后的 writer 传入 next.ServeHTTP,而不能只传入原始 writer。
    • 通过: 代码片段显示,在 next 返回后观察状态时,使用 next.ServeHTTP(wrapped, r)(或等效写法)。
  3. 每条继续处理的路径都会调用 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。

评论

支持 Markdown