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 を参照してください。
リカバリーミドルウェア
パニックを捕捉して、単一の不正なリクエストによってサーバーがクラッシュするのを防ぎます:
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)
推奨される順序
- Recovery -- 最外側。すべての内側のミドルウェアからのパニックを捕捉します
- RequestID -- 後続のすべてのミドルウェアが参照できるよう、早い段階で割り当てます
- Logger -- ID とステータスを含め、完了したリクエストをログに記録します
- Auth -- 認証失敗も記録されるよう、ロギングの後に配置します
- アプリケーション固有のミドルウェア -- レート制限、CORS など
ゲート(マージまたはレビューの前に確認)
客観的な合否判定には、以下の順序付きチェックを使用してください。「頭の中で確認しました」で置き換えないでください。
- リカバリーの位置
- サーバーがミドルウェアチェーンを構築する場所(例:
main、ルーターのUse、またはChainヘルパー)を特定します。 - 合格: ミドルウェアチェーンの順序に従って、リカバリーが他のすべてのミドルウェアと最終ハンドラーをラップしています(ネスト形式では最も外側、またはヘルパーの
Chain引数の順序が正しいこと)。ファイルパスとチェーン全体のスニペットを示してください。
- サーバーがミドルウェアチェーンを構築する場所(例:
- ステータスを考慮するミドルウェアではラップされた
ResponseWriterを使用する- ミドルウェアがハンドラーの実行後に HTTP ステータスをログに記録する、または記録する場合は、元のライターだけでなく、ラッパーを
next.ServeHTTPに渡す必要があります。 - 合格:
nextが戻った後にステータスを確認する場合、スニペットにnext.ServeHTTP(wrapped, r)(または同等の処理)が示されています。
- ミドルウェアがハンドラーの実行後に HTTP ステータスをログに記録する、または記録する場合は、元のライターだけでなく、ラッパーを
- すべての順方向の経路で
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 を通じて渡す横断的なメタデータのためのものであり、関数シグネチャを避けるためのものではありません。
リカバリーミドルウェアの位置が誤っている
リカバリーが最も外側のミドルウェアでない場合、外側のミドルウェアで発生したパニックによりサーバーがクラッシュします。必ずリカバリーを最初に適用してください。