Go のミドルウェアを自作する:http.Handler のラップパターン

Go の HTTP ミドルウェアは、ハンドラを受け取って新しいハンドラを返す関数です。ログ出力、認証チェック、CORS 設定など、複数のエンドポイントに共通する処理を一箇所にまとめられます。

ミドルウェアの基本パターン

ミドルウェアの本質は「http.Handler を受け取り、http.Handler を返す関数」です。内部で元のハンドラの前後に処理を差し込みます。

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

next.ServeHTTP(w, r) を呼ぶことで、元のハンドラに処理を引き継いでいます。この呼び出しの前に書いた処理が「前処理」、後に書いた処理が「後処理」になります。

リクエスト受信

ミドルウェアの前処理(ログ出力など)

元のハンドラ実行

ミドルウェアの後処理(計測など)

実際に使ってみる

作ったミドルウェアは、ハンドラをラップするように適用します。

package main

import (
	"fmt"
	"net/http"
)

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "Hello, World!")
}

func main() {
	hello := http.HandlerFunc(helloHandler)
	http.Handle("/hello", loggingMiddleware(hello))
	http.ListenAndServe(":8080", nil)
}

/hello にアクセスするたびに、ターミナルに [GET] /hello のようなログが出力されます。ハンドラの中身を変更せずにログ機能を追加できるのがミドルウェアの強みです。

複数のミドルウェアを重ねる

ミドルウェアは何層でも重ねられます。認証チェックとログ出力を両方適用したい場合は、入れ子にして書きます。

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", http.StatusUnauthorized)
			return
		}
		next.ServeHTTP(w, r)
	})
}

func main() {
	hello := http.HandlerFunc(helloHandler)
	wrapped := loggingMiddleware(authMiddleware(hello))
	http.Handle("/hello", wrapped)
	http.ListenAndServe(":8080", nil)
}

この場合、リクエストはまず loggingMiddleware を通り、次に authMiddleware で認証チェックされ、通過すれば helloHandler が実行されます。ミドルウェアの適用順序はそのまま実行順序になるので、どの順番で重ねるかは意識しておきましょう。

authMiddleware では Authorization ヘッダーが空の場合に http.Error でレスポンスを返し、return で処理を終了しています。next.ServeHTTP を呼ばなければ、後続のハンドラは実行されません。これがミドルウェアでリクエストをブロックする仕組みです。

チェーン関数で見通しをよくする

ミドルウェアが増えると入れ子が深くなり、読みづらくなります。チェーン用のヘルパー関数を作ると、適用順序が明確になります。

func chain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
	for i := len(middlewares) - 1; i >= 0; i-- {
		h = middlewares[i](h)
	}
	return h
}

func main() {
	hello := http.HandlerFunc(helloHandler)

	wrapped := chain(hello,
		loggingMiddleware,
		authMiddleware,
	)

	http.Handle("/hello", wrapped)
	http.ListenAndServe(":8080", nil)
}

chain 関数はスライスの末尾から順にミドルウェアを適用しています。こうすることで、呼び出し側では上から下に書いた順番がそのまま実行順序になります。

入れ子方式

loggingMiddleware(authMiddleware(hello)) のように書く。少数なら問題ないが、5 個以上になると読みづらい。

チェーン方式

chain(hello, logging, auth, cors) のように並べる。適用順序がリストとして可視化され、追加・削除も容易。

全ルートにミドルウェアを適用する

個別のハンドラではなく、サーバー全体にミドルウェアを適用したい場合もあります。http.ListenAndServe の第 2 引数にミドルウェアでラップしたマルチプレクサを渡せば、全リクエストに適用されます。

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", helloHandler)
	mux.HandleFunc("/health", healthHandler)

	wrapped := loggingMiddleware(mux)
	http.ListenAndServe(":8080", wrapped)
}

http.NewServeMux で新しいマルチプレクサを作り、そこにルーティングを登録してからミドルウェアでラップする形です。DefaultServeMux を使わないため、グローバルな状態に依存しないクリーンな構成になります。

ミドルウェアパターンは Go の HTTP プログラミングの基礎です。chi や Echo などのフレームワークでもまったく同じ考え方が使われているので、標準ライブラリで仕組みを理解しておけば、どのフレームワークに移っても応用が利きます。