Goのミドルウェアパターンを理解する—net/httpで柔軟なリクエスト処理を実装する方法

Goのミドルウェアパターンを理解する—net/httpで柔軟なリクエスト処理を実装する方法 | mohablog
目次

はじめに

Goでウェブアプリケーションを構築していると、認証チェック、ロギング、CORSヘッダーの追加など、複数のエンドポイントで共通の処理が必要になる場面が増えていきます。最初は各ハンドラーに同じコードを書いていたのですが、すぐに保守性が落ちることに気づきました。そこで活躍するのがミドルウェアパターンです。

このパターンを理解することで、コードの再利用性を高め、関心の分離を実現でき、本番環境のプロジェクトでも安心して使える設計ができるようになります。本記事では、net/httpパッケージを使ったミドルウェアの実装方法と、実務で頻出のパターンを具体的に解説していきます。

ミドルウェアとは何か

ミドルウェアは、HTTPリクエストがハンドラーに到達する前、または離れた後に、共通の処理を挿入する仕組みです。公式ドキュメントではミドルウェアについて明確には言及していませんが、http.Handlerインターフェースを上手く活用することで、簡潔に実装できます。

基本的なミドルウェアの構造

Goのミドルウェアは「ハンドラーを受け取り、新しいハンドラーを返す関数」という形で実装します。これを関数型の高階関数パターンと呼びます。

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("Request: %s %s\n", r.Method, r.RequestURI)
        next.ServeHTTP(w, r)
    })
}

このコードでは、nextという次のハンドラーを受け取り、それをラップした新しいハンドラーを返しています。リクエストが到達すると、まずログを出力してから、元のハンドラー(next)に処理を渡します。

なぜこのパターンが有効か

ミドルウェアを使うことで、以下のメリットが得られます:

  • 複数のハンドラーで共通処理を共有でき、コード重複を減らせる
  • ログ出力、認証、エラーハンドリングなど、クロスカッティングな関心事を分離できる
  • 新しい処理を追加するときも、既存のハンドラーを変更せず済む
  • ミドルウェアの順序を簡単に変更でき、処理フローを柔軟に制御できる

アンチパターン:ハンドラーに直接コードを書く

まず、ミドルウェアを使わない場合の問題を見てみます。

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    // ログ出力
    fmt.Printf("Request: %s %s\n", r.Method, r.RequestURI)
    
    // 認証チェック
    token := r.Header.Get("Authorization")
    if token == "" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    // 実際の処理
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"user_id": 1}`)
}

func getPostHandler(w http.ResponseWriter, r *http.Request) {
    // ログ出力
    fmt.Printf("Request: %s %s\n", r.Method, r.RequestURI)
    
    // 認証チェック
    token := r.Header.Get("Authorization")
    if token == "" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    // 実際の処理
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"post_id": 1}`)
}

ご覧の通り、ログ出力と認証チェックが両方のハンドラーに重複しています。エンドポイントが増えると、この重複は加速度的に増えていき、保守が困難になります。認証ロジックを変更したいときは、全てのハンドラーを修正する必要があります。

ミドルウェアパターンの実装方法

1. シンプルなロギングミドルウェア

まずは基本的なロギングミドルウェアから始めます。

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        duration := time.Since(start)
        fmt.Printf("[%s] %s %s - %dms\n", r.Method, r.RequestURI, r.RemoteAddr, duration.Milliseconds())
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"user_id": 1}`)
    })
    
    // ミドルウェアを適用
    handler := loggingMiddleware(mux)
    http.ListenAndServe(":8080", handler)
}
[GET] /api/user 127.0.0.1 - 2ms

この実装では、リクエスト処理にかかった時間を計測してログに記録しています。time.Now()で処理開始前の時刻を記録し、処理後に経過時間を計算することで、パフォーマンスモニタリングが容易になります。

2. 認証ミドルウェア

次に、認証チェックを行うミドルウェアです。

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized: missing token", http.StatusUnauthorized)
            return
        }
        
        // トークン検証(簡略版)
        if !isValidToken(token) {
            http.Error(w, "Unauthorized: invalid token", http.StatusUnauthorized)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

func isValidToken(token string) bool {
    // 実際にはJWTやセッションの検証を行う
    return token == "valid-token-12345"
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"user_id": 1}`)
    })
    
    // 認証を必須にするミドルウェアを適用
    handler := authMiddleware(mux)
    http.ListenAndServe(":8080", handler)
}
GET /api/user HTTP/1.1
Authorization: 

401 Unauthorized: missing token

---

GET /api/user HTTP/1.1
Authorization: valid-token-12345

200 OK
{"user_id": 1}

3. ミドルウェアの連鎖(チェーン)

複数のミドルウェアを組み合わせたいときは、どのように実装するでしょうか。まずはシンプルなアプローチです。

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"user_id": 1}`)
    })
    
    // ミドルウェアを順番に適用
    handler := loggingMiddleware(authMiddleware(mux))
    http.ListenAndServe(":8080", handler)
}

このコードでは、authMiddlewareが内側にあり、loggingMiddlewareが外側にあります。つまり、リクエストは以下の順序で処理されます:

  1. ロギングミドルウェアが処理開始を記録
  2. 認証ミドルウェアがトークンを検証
  3. 実際のハンドラーが処理を実行
  4. ロギングミドルウェアが処理時間を記録

4. ミドルウェアの共通化:Chain関数を作る

複数のミドルウェアを適用するたびに入れ子を増やしていくのは、可読性が低くなります。そこで、ミドルウェアをチェーンできる便利な関数を作ると良いでしょう。

// Chain は複数のミドルウェアを組み合わせる
func Chain(handler http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middleware) - 1; i >= 0; i-- {
        handler = middleware[i](handler)
    }
    return handler
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"user_id": 1}`)
    })
    
    // 複数のミドルウェアを見やすく適用
    handler := Chain(mux, loggingMiddleware, authMiddleware)
    http.ListenAndServe(":8080", handler)
}

こうすることで、ミドルウェアの追加や削除が簡単になり、処理順序も明確になります。

実務で頻出のミドルウェアパターン

CORSミドルウェア

モダンなウェブアプリケーションでは、異なるオリジンからのリクエストが多いため、CORSヘッダーの設定は必須です。

func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        // OPTIONS リクエストには本体がないため、ここで終了
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }
        
        next.ServeHTTP(w, r)
    })
}

OPTIONSメソッドは、ブラウザがプリフライトリクエストを送信するときに使われます。このリクエストに対しては、ハンドラーを実行せず、CORSヘッダーだけを返すのが正しい動作です。

リカバリーミドルウェア(パニック対策)

ハンドラー内でパニックが発生した場合、サーバー全体が止まってしまいます。本番環境ではこれを防ぐことが重要です。

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                fmt.Printf("Panic recovered: %v\n", err)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

deferrecover()を組み合わせることで、パニックをキャッチして、適切なエラーレスポンスを返しています。

レート制限ミドルウェア

APIの過負荷を防ぐため、クライアントごとにリクエスト数を制限することがあります。以下は簡単な実装例です。

import (
    "sync"
    "time"
)

type RateLimiter struct {
    clients map[string][]time.Time
    mu      sync.Mutex
    limit   int
    window  time.Duration
}

func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
    return &RateLimiter{
        clients: make(map[string][]time.Time),
        limit:   limit,
        window:  window,
    }
}

func (rl *RateLimiter) isAllowed(clientIP string) bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()
    
    now := time.Now()
    cutoff := now.Add(-rl.window)
    
    // 古いタイムスタンプを削除
    var recent []time.Time
    for _, t := range rl.clients[clientIP] {
        if t.After(cutoff) {
            recent = append(recent, t)
        }
    }
    
    if len(recent) >= rl.limit {
        rl.clients[clientIP] = recent
        return false
    }
    
    recent = append(recent, now)
    rl.clients[clientIP] = recent
    return true
}

func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        clientIP := r.RemoteAddr
        if !rl.isAllowed(clientIP) {
            http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"status": "ok"}`)
    })
    
    limiter := NewRateLimiter(10, time.Minute) // 1分間に10リクエストまで
    handler := Chain(mux, limiter.Middleware, loggingMiddleware)
    http.ListenAndServe(":8080", handler)
}
GET /api/data -> 200 OK
GET /api/data -> 200 OK
... (10回まで)
GET /api/data -> 429 Too Many Requests

コンテキストを使ったミドルウェア

ミドルウェアで設定した値をハンドラーで利用する場合、context.Contextを使うのが慣例です。

import "context"

type contextKey string

const userIDKey contextKey = "userID"

func authContextMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        var userID string
        
        if token == "valid-token-12345" {
            userID = "user-123"
        } else {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        // コンテキストに値を埋め込む
        ctx := context.WithValue(r.Context(), userIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handleUserData(w http.ResponseWriter, r *http.Request) {
    // コンテキストから値を取得
    userID := r.Context().Value(userIDKey).(string)
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintf(w, `{"user_id": "%s"}`, userID)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/user", handleUserData)
    
    handler := Chain(mux, authContextMiddleware)
    http.ListenAndServe(":8080", handler)
}

これにより、認証情報をハンドラーで簡単にアクセスできるようになります。

ミドルウェアの設計時の注意点

ミドルウェアの順序が重要

ミドルウェアの順序によって、動作が大きく変わります。たとえば、ロギングの前に認証を置くと、未認証のリクエストもログに記録されます。セキュリティと監視の要件に応じて、順序を慎重に決めましょう。

ミドルウェアの責任は単一にする

1つのミドルウェアは1つのことだけを行うべきです。ロギング、認証、キャッシュなどを1つのミドルウェアに詰め込むと、テストや再利用が困難になります。

エラーハンドリングの一貫性

複数のミドルウェアがエラーを返す場合、レスポンスフォーマット(JSONか平文か)を統一することが重要です。さもないと、クライアント側で対応が複雑になります。関連として、「Goのエラーハンドリングで失敗しない方法」という記事も参考になります。

パフォーマンス上の考慮

ミドルウェアが重い処理を行う場合、全てのリクエストに影響します。たとえば、認証ミドルウェアでデータベースへのアクセスが発生する場合、キャッシュやトークンの事前検証を検討するとよいでしょう。

よくあるハマりどころ

ResponseWriterの二重ラップ

ミドルウェアの中でResponseWriterをカスタムラップする場合、ステータスコードやヘッダーの書き込みが二重に行われないよう注意が必要です。

// NG な例
func badMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK) // ここでヘッダーを送信
        next.ServeHTTP(w, r) // ハンドラーが再度書き込みを試みる → エラー
    })
}

// OK な例
type wrappedWriter struct {
    http.ResponseWriter
    statusCode int
}

func (w *wrappedWriter) WriteHeader(statusCode int) {
    w.statusCode = statusCode
    w.ResponseWriter.WriteHeader(statusCode)
}

func goodMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        wrapped := &wrappedWriter{ResponseWriter: w}
        next.ServeHTTP(wrapped, r)
    })
}

まとめ

  • ミドルウェアは「ハンドラーを受け取り、新しいハンドラーを返す関数」として実装する
  • http.HandlerFuncを使うことで、シンプルで読みやすいミドルウェアが書ける
  • 複数のミドルウェアはChain関数を使ってスッキリと組み合わせられる
  • ロギング、認証、CORS、レート制限、パニック対策など、実務で必要なミドルウェアはパターン化できる
  • ミドルウェアの順序、責任の分離、エラーハンドリングの一貫性に注意することが、保守しやすいコードにつながる
  • context.Contextを使うことで、ミドルウェア間のデータ受け渡しが安全になる

よくある質問(FAQ)

Q1: ミドルウェアとフィルターの違いは何ですか?

Goでは「ミドルウェア」という用語が主に使われます。フレームワークによっては「フィルター」と呼ぶこともありますが、概念は同じです。リクエストとレスポンスの処理フローに割り込んで、共通処理を実行する仕組みという点では一致しています。

Q2: ミドルウェアでレスポンスボディを読み込みたい場合はどうしますか?

デフォルトではr.Bodyを読むと、ハンドラーで再度読むことができません。対策として、io.TeeReaderを使ってボディを複製するか、r.Bodyio.NopCloserでラップして再度読ませることができます。ただし、パフォーマンスへの影響を考慮し、必要な場合だけ使用しましょう。

Q3: サードパーティーのルーターライブラリを使う場合、ミドルウェアはどのように実装しますか?

ChiやGinなどのルーターでも、ミドルウェアのインターフェースは同じくhttp.Handlerを基本としています。ライブラリが提供するUse()メソッドなどでミドルウェアを登録できます。本記事のChain関数や基本パターンは、ほとんどのライブラリで応用できます。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次