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 を参照してください。

リカバリーミドルウェア

パニックを捕捉して、単一の不正なリクエストによってサーバーがクラッシュするのを防ぎます:

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)
    })
}

すべての内側のミドルウェアとハンドラーからのパニックを捕捉できるよう、リカバリーは最外側のミドルウェアにする必要があります。詳細は 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 -- 最外側。すべての内側のミドルウェアからのパニックを捕捉します
  2. RequestID -- 後続のすべてのミドルウェアが参照できるよう、早い段階で割り当てます
  3. Logger -- ID とステータスを含め、完了したリクエストをログに記録します
  4. Auth -- 認証失敗も記録されるよう、ロギングの後に配置します
  5. アプリケーション固有のミドルウェア -- レート制限、CORS など

ゲート(マージまたはレビューの前に確認)

客観的な合否判定には、以下の順序付きチェックを使用してください。「頭の中で確認しました」で置き換えないでください。

  1. リカバリーの位置
    • サーバーがミドルウェアチェーンを構築する場所(例:main、ルーターの Use、または Chain ヘルパー)を特定します。
    • 合格: ミドルウェアチェーンの順序に従って、リカバリーが他のすべてのミドルウェアと最終ハンドラーをラップしています(ネスト形式では最も外側、またはヘルパーの Chain 引数の順序が正しいこと)。ファイルパスとチェーン全体のスニペットを示してください。
  2. ステータスを考慮するミドルウェアではラップされた ResponseWriter を使用する
    • ミドルウェアがハンドラーの実行後に HTTP ステータスをログに記録する、または記録する場合は、元のライターだけでなく、ラッパーを next.ServeHTTP に渡す必要があります。
    • 合格: 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 を通じて渡す横断的なメタデータのためのものであり、関数シグネチャを避けるためのものではありません。

リカバリーミドルウェアの位置が誤っている

リカバリーが最も外側のミドルウェアでない場合、外側のミドルウェアで発生したパニックによりサーバーがクラッシュします。必ずリカバリーを最初に適用してください。

コメント