Go実践 #5 ミドルウェアパターン
#4 DB連携で内側のデータを扱ったなら — 今度は再び外側。リクエストとレスポンスの間の共通処理。
Goのミドルウェアは — 他言語のミドルウェアと本質は同じですが、言語が別途サポートする概念ではなくhttp.Handlerインターフェースだけで表現されるのが特徴です。シンプルなぶん — 強力です。
ミドルウェアの形 — Handler → Handler
#
type Middleware func(http.Handler) http.Handler関数1つが — ハンドラを受けて新しいハンドラを返す。その中で元のハンドラを呼ぶ前後に仕事をします。
最小の例 — ロギング #
func logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}中核:
http.HandlerFuncアダプタで関数をHandlerに変換next.ServeHTTP(w, r)が実際のハンドラ呼び出し — その前後にロジック
適用 #
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", helloHandler)
handler := logger(mux) // mux全体をロギングで包む
http.ListenAndServe(":8080", handler)mux自体もhttp.Handler — ミドルウェアで囲むのが自然です。
チェーン #
複数のミドルウェアを — 関数合成のように。
handler := logger(recoverer(authMiddleware(mux)))順序: logger → recoverer → auth → mux。内側が先に実行されるのではなく — 外側が先にServeHTTPに入る。呼び出しフローは:
logger.ServeHTTP開始(start記録)- →
recoverer.ServeHTTP開始(defer recover) - →
auth.ServeHTTP開始(認証検査) - →
mux.ServeHTTP(実際のルーティング) - ← レスポンス後に順番に抜ける(ログ記録など)
関数合成の形なので — 順序が重要。通常outerからinner: ログ → 復旧 → 認証 → ルーティング。
チェーンヘルパー #
手でネストすると可読性が落ちます。ヘルパー1つで平たく。
type Middleware func(http.Handler) http.Handler
func chain(h http.Handler, mw ...Middleware) http.Handler {
for i := len(mw) - 1; i >= 0; i-- {
h = mw[i](h)
}
return h
}
handler := chain(mux, logger, recoverer, authMiddleware)リストの前方が — 外側(先に実行)。
標準ミドルウェア集 #
1) Recoverer — パニック復旧 #
func recoverer(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("panic: %v\n%s", err, debug.Stack())
http.Error(w, "internal error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}ハンドラでpanicが起きたら — サーバーが死なないように捕まえて500レスポンス。すべてのハンドラを包む最も外側のミドルウェアの1つ。
2) Request ID #
type ctxKey string
const reqIDKey ctxKey = "reqID"
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()
}
w.Header().Set("X-Request-ID", id)
ctx := context.WithValue(r.Context(), reqIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func RequestIDFrom(ctx context.Context) string {
if v, ok := ctx.Value(reqIDKey).(string); ok {
return v
}
return ""
}分散システムで — 1つのリクエストを追跡するための標準。contextに入れておいて — ログに含めます。
中級#5のctx.Valueが似合う場面の中で最も一般的なケース。
3) レスポンスステータスのキャプチャ — accesslog拡張 #
デフォルトのResponseWriterは — レスポンスステータスを記憶しません。ログにステータス/サイズを入れたいなら小さなラッパー。
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
func (r *statusRecorder) Write(b []byte) (int, error) {
if r.status == 0 {
r.status = http.StatusOK
}
n, err := r.ResponseWriter.Write(b)
r.bytes += n
return n, err
}
func accessLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rec := &statusRecorder{ResponseWriter: w}
start := time.Now()
next.ServeHTTP(rec, r)
log.Printf("%s %s %d %dB %s",
r.Method, r.URL.Path, rec.status, rec.bytes, time.Since(start))
})
}このパターンが — chi、ginのようなフレームワークの中にも同じようにあります。自分で書いてみたなら、そのコードも親しみやすくなります。
4) 認証 #
type userKey struct{}
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
}
user, err := verifyToken(token)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey{}, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func UserFrom(ctx context.Context) (User, bool) {
u, ok := ctx.Value(userKey{}).(User)
return u, ok
}ctxキーは — 外部に公開されない非公開型で。他パッケージの同名stringキーと衝突しません。
5) CORS #
func cors(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")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}プロダクションでは — Origin検証、credentials処理などがさらに入ります。rs/corsパッケージが標準に近く処理。
6) Rate limit #
import "golang.org/x/time/rate"
func limit(next http.Handler) http.Handler {
limiter := rate.NewLimiter(10, 30) // 10 req/sec, burst 30
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "too many", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}サーバー全体単位。ユーザー別に制限するなら — IP/ユーザーID別のlimiter map。大きなトラフィックならRedisベースの分散limiterを検討。
ルート単位ミドルウェア — 標準ServeMuxの限界 #
標準ServeMuxは — ルート単位でミドルウェアを挟むヘルパーがありません。手で包む形。
mux.HandleFunc("GET /admin", authMiddleware(http.HandlerFunc(adminHandler)).ServeHTTP)ルートが多くなるとボイラープレートが気になる — chiのようなルーターのr.Useが自然になる場面。
r.Group(func(r chi.Router) {
r.Use(authMiddleware)
r.Get("/admin", adminHandler)
r.Get("/me", meHandler)
})#2のルーター選択トレードオフと同じ文脈。
標準パターン集 #
典型的なサーバーのミドルウェアスタック:
handler := chain(mux,
requestID, // 1) リクエストIDを付与
accessLog, // 2) アクセスログ
recoverer, // 3) パニック復旧
cors, // 4) CORSヘッダー
limit, // 5) Rate limit
// authはルーター別に
)順序は — recovererが内側にあったらいいけれど、それだとその上のミドルウェアのpanicが捕まえられません。ログ → 復旧 → その他の順序が普通。
落とし穴 — r.WithContextを渡さない
#
func mw(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), key, val)
next.ServeHTTP(w, r) // ✗ 新しいctxを渡さない
})
}r.WithContext(ctx)で — 新しいRequestを作って渡す必要があり、コンテキスト値が伝わります。
next.ServeHTTP(w, r.WithContext(ctx))落とし穴 — next.ServeHTTPの漏れ
#
条件付き認証のような場面で — 認証失敗ならレスポンスを送ってreturn。認証成功ならnext.ServeHTTPを呼ぶ。2つの経路が混乱すると — レスポンスが2回出るかまったく出ないバグ。
ミドルウェアのよいデザイン #
- 単一責任 — 1つのミドルウェアに1つの仕事
- 順序の意図を明示 — コメントで書いておく
- 共有状態を最小化 — 可能ならctxだけを通す
- エラーはレスポンスで表現 — ミドルウェア自体がpanicしないように
- ミドルウェアもテスト —
httptestでシンプルに検証
まとめ #
この記事で整理した内容:
- ミドルウェア =
func(http.Handler) http.Handler - ロガー、復旧、リクエストID、認証、CORS、rate limit — 標準ビルディングブロック
- チェーンヘルパー — 平たい登録
- 順序 — 外側が先に実行、通常ログ → 復旧 → その他
r.WithContextを忘れない- status recorderでレスポンスステータス/サイズをキャプチャ
- 標準muxはルート単位ミドルウェアが弱い — chiが滑らか
次の記事(#6 テストとデプロイ)では — httptestでハンドラテスト、Dockerマルチステージビルド、そして小さなサーバーを運用環境に乗せる標準パターンを整理します。