2-4. ミドルウェア - 合成・順序・context 連携
所要時間: 40-60分(2セッション分) コミット内容:
~/learn/go/level2/day04/にログ・recover・request ID・タイムアウトの自作ミドルウェア群
このレッスンのゴール
-
func(http.Handler) http.Handlerシグネチャを書ける - RequestID/Recover/AccessLog/Timeout 4つを自作できる
- ミドルウェア順序の設計判断ができる
-
context.WithValueでリクエストスコープ値を伝播 -
statusRecorderで実ステータスをログに残せる
なぜ学ぶか
「全エンドポイントに認証チェックを入れたい」「全リクエストにIDを振りログを追跡可能にしたい」「panicでサーバー落ちないようにrecoverを仕掛けたい」 - これらを 各ハンドラに書くと地獄。ミドルウェアで「ハンドラの周りに層」を被せるのが解。本章は Level 2 で最も「Go らしさ」を体感できる回。http.Handler インターフェースの真価がここで開花する。
前章とのつながり
2-1_net_http で http.Handler インターフェースを学んだ。本章はそれを 「ハンドラを受け取って別のハンドラを返す関数」 という形で活用する。http.HandlerFunc キャストの存在意義もここで明白になる。2-3_JSON の decodeJSON は 本質的にミドルウェアの一部 だったと気づく。
これができると何が嬉しいか
- 障害解析が劇的に楽になる - request_id で grep 一発で1リクエストの全ログ
- panic でサーバーが落ちない - recover ミドルウェアで境界防御
- 認証/レート制限/CORS を1行のコードで全エンドポイントに追加できる
- chi/gin のミドルウェア記法に違和感がない - 全部同じパターン
ストーリー導入: ミドルウェアは「玉ねぎの皮」
ハンドラ本体(玉ねぎの芯)を、ログ→recover→認証→…と 皮で何重にも包む。リクエストは外側の皮から順に入って、芯で処理されて、また外側の皮を逆順に通って出ていく。これが Go の http.Handler の合成パターン。Decorator パターン の教科書的実装。順序を間違えると「recover の外でログを書こうとして自分が panic する」みたいな悲劇が起きるので設計が重要。
大前提: ミドルウェアは「ハンドラを包む層」
すべてのリクエストに共通でやりたい処理がある:
- アクセスログ
- panic からの復帰(recover)
- 認証・認可
- リクエスト ID 採番
- レート制限
- CORS ヘッダ追加
- リクエストタイムアウト
これらをハンドラごとに書くのは無理。ハンドラの周りに「層」として被せるのが解。それがミドルウェア。
Express でいう app.use((req, res, next) => {...})、Rails でいう Rack middleware、Java でいう Servlet Filter と同じ概念。Go では http.Handler を入れ子にすることで実現する。
[クライアント] → [ログ層] → [recover層] → [認証層] → [ハンドラ本体]
↓ ↑
└──────────── 全リクエスト共通 ────────┘
セッション①: ミドルウェアパターンの本質(30分)
0. 録画スタート&作業ディレクトリ
mkdir -p ~/log ~/learn/go/level2/day04
cd ~/learn/go/level2/day04
script ~/log/go_level2_day04.log
# Go モジュール初期化
go mod init example.com/level2/day041. 最小のミドルウェア
package main
import (
"log"
"net/http"
"time"
)
// ミドルウェアの「型」: http.Handler を受け取って http.Handler を返す関数
type Middleware func(http.Handler) http.Handler
// アクセスログを取るミドルウェア
func Logging(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 %v", r.Method, r.URL.Path, time.Since(start))
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
})
// mux を Logging で包む
wrapped := Logging(mux)
http.ListenAndServe(":8080", wrapped)
}go run main.go &
curl http://localhost:8080/hello
# サーバー側ログ: GET /hello 123.4µsミドルウェアの本質
ミドルウェアは 「
http.Handlerを受け取ってhttp.Handlerを返す関数」。シグネチャは:func MiddlewareName(next http.Handler) http.Handler
nextは「自分の次に呼ばれるハンドラ」。next.ServeHTTP(w, r)を呼ぶことで処理を委譲する。これは Decorator パターン(GoF デザインパターン)の典型的な実装。元のハンドラに機能を「追加」していく。
http.HandlerFunc(func(w, r) {...}) のキャストが鍵
http.HandlerFunc(func)は「関数をhttp.Handlerに変換」するアダプタ(2-1 で学んだやつ)。これがあるから、無名関数を Handler として返せる。もしこれが無ければ、毎回 struct を定義して
ServeHTTPメソッドを生やすことになる。HandlerFuncの存在がミドルウェア記述を簡潔にしている。
2. ミドルウェアを「重ねる」
func Auth(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 を呼ばない = 後続を実行しない
}
next.ServeHTTP(w, r)
})
}
// Logging → Auth → mux の順に通る
wrapped := Logging(Auth(mux))
http.ListenAndServe(":8080", wrapped)包む順序 = 実行順序の外側から内側
Logging(Auth(mux))実行の流れ:
- Logging の「next.ServeHTTP 前」処理(時刻記録)
- Auth の「next.ServeHTTP 前」処理(トークン確認)
- mux → ハンドラ本体
- Auth の「next.ServeHTTP 後」処理(あれば)
- Logging の「next.ServeHTTP 後」処理(ログ出力)
外側のミドルウェアが最初と最後に呼ばれる。サンドイッチ構造。
3. ミドルウェアの順序設計
推奨される標準的な順序(外側→内側)
[リクエストID採番] ← 最初に必ず ID を振る(後段のログで使う) ↓ [アクセスログ] ← 全リクエストを必ず記録 ↓ [recover (panic 回復)] ← 万一のpanicで500を返す ↓ [リクエストタイムアウト] ← 過剰に長い処理を止める ↓ [CORS] ← プリフライト OPTIONS は早めに返す ↓ [レート制限] ← 認証通すコストを節約 ↓ [認証] ← Bearer Token、Cookie などの検証 ↓ [認可] ← 認証されたユーザーがこのリソースを触れるか ↓ [ハンドラ本体]
順序を間違えると起きる事故
- ログより先に recover を置くと: panic 時のリクエストがログに残らない(recover で握りつぶされ、ログミドルウェアが panic に気づけない)
- 認証より先にレート制限を置かないと: 認証コストが先に発生する → DoS 攻撃で CPU を食われる
- CORS より先に認証を置くと: ブラウザのプリフライト OPTIONS が認証失敗 → CORS エラーで真の原因が見えなくなる
- タイムアウトを最内側に置くと: ミドルウェアの遅延(DBアクセスする認証など)でタイムアウト超過してから止まる
「全リクエストに走らせたいもの」ほど外側、「特定の処理に近いもの」ほど内側が原則。
4. recover ミドルウェア(重要)
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
stack := debug.Stack()
slog.Error("panic recovered",
"err", rec,
"path", r.URL.Path,
"stack", string(stack),
)
// クライアントには汎用エラーだけ返す(内部情報を漏らさない)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}recover ミドルウェアが無いと、panic でサーバー全体が落ちるか?
落ちない(Go 1.x 系の
net/httpは標準で recover している)。標準ライブラリ内部で goroutine ごとに recover してくれる。しかし標準の recover は
Connection: closeを付けて 500 を返すだけ。ログにも残らない。自前の Recover ミドルウェアの価値:
- panic 内容とスタックトレースを構造化ログに残せる → 障害解析の生命線
- クライアントに返すエラーメッセージを制御できる(標準は空ボディ)
- panic を計測指標として観測可能にできる(Prometheus に panic_total を出す等)
アンチパターン: panic 情報をそのままクライアントに返す
if rec := recover(); rec != nil { http.Error(w, fmt.Sprintf("error: %v\n%s", rec, debug.Stack()), 500) }なぜNG: スタックトレースには ファイルパス、関数名、内部の DB クエリ文字列 など、攻撃者にとって有用な情報が含まれる。これを返すと「攻撃の地図を渡す」ことになる。
修正: スタックは サーバー側ログに、クライアントには 汎用エラー だけ返す。
5. リクエスト ID ミドルウェアと context 連携
type ctxKey string
const requestIDKey ctxKey = "requestID"
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 既に上流(LB やリバプロ)で付与されていれば尊重
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.NewString() // github.com/google/uuid
}
// context に積む
ctx := context.WithValue(r.Context(), requestIDKey, id)
r = r.WithContext(ctx)
// レスポンスヘッダにも返す
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r)
})
}
// ハンドラから取り出す
func GetRequestID(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return ""
}context.Context は「リクエストスコープの値の運び屋」
Go 1.7+ で標準導入された
contextパッケージは、HTTP リクエストの「寿命」と「リクエスト固有の値」をハンドラ間で受け渡しするための仕組み。使うもの:
- キャンセル通知:
ctx.Done()で「クライアントが切断した」「親 context がタイムアウト」を検知- デッドライン:
ctx.WithTimeoutで「5秒で諦める」設定- 値伝播:
ctx.WithValueでリクエスト ID、ユーザー ID、ロガーなどを乗せるミドルウェアは
r.Context()から派生 context を作り、r = r.WithContext(...)で後段に渡す。
context.WithValue の使いすぎは禁止
公式ドキュメントに 「リクエストスコープのデータ以外を入れるな」 と明記されている。
OK: リクエスト ID、認証済みユーザー ID、トレース ID NG: DB コネクション、ロガー(依存注入で渡すべき)、設定値(起動時に渡すべき)
context は 動的な値の運搬路 であって、依存注入の代わりではない。
6. 自前ロガーを context に乗せる
type loggerCtxKey struct{}
func InjectLogger(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// リクエスト ID を取り出して、フィールド付きロガーを作る
requestID := GetRequestID(r.Context())
rlog := logger.With("request_id", requestID, "path", r.URL.Path)
ctx := context.WithValue(r.Context(), loggerCtxKey{}, rlog)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ハンドラから取り出す
func LoggerFromCtx(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(loggerCtxKey{}).(*slog.Logger); ok {
return l
}
return slog.Default()
}
// 使う側
func myHandler(w http.ResponseWriter, r *http.Request) {
log := LoggerFromCtx(r.Context())
log.Info("processing", "user_id", 42)
// → {"time":..., "msg":"processing", "request_id":"abc-123", "path":"/x", "user_id":42}
}なぜ「リクエスト ID 付きロガー」を context に積むか
1リクエスト = 1 goroutine + その中で複数の関数呼び出し → ログが時系列で混ざる。
リクエスト ID + ロガーが context に乗っていれば、全てのログ行に自動で request_id が付く → 障害時に「このリクエストで何が起きたか」を grep 一発で追える。
Stripe, Cloudflare などの本番運用ベストプラクティス。
セッション②: ResponseWriter のラップとよくある事故(30分)
7. ResponseWriter をラップして「実際のステータスコード」を記録する
ログには「200 を返した」「404 を返した」を残したい。しかし、標準の ResponseWriter は 書いたステータスを後から取り出すメソッドが無い。自前でラップする必要がある。
type statusRecorder struct {
http.ResponseWriter
status int
bytes int
}
func (s *statusRecorder) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func (s *statusRecorder) Write(b []byte) (int, error) {
if s.status == 0 {
s.status = http.StatusOK // WriteHeader を呼ばずに Write した場合の暗黙の 200
}
n, err := s.ResponseWriter.Write(b)
s.bytes += n
return n, err
}
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w}
next.ServeHTTP(rec, r)
slog.Info("http",
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"bytes", rec.bytes,
"duration_ms", time.Since(start).Milliseconds(),
"request_id", GetRequestID(r.Context()),
)
})
}このラップが必要な理由
アクセスログは「200 か 404 か 500 か」が 最重要メトリクス。これが取れなかったら、エラー率の監視も SLO 計測も成り立たない。
Go の
http.ResponseWriterは 書き込み専用 のインターフェースで、「書いた内容」を読み出す方法は標準では用意されていない。自前ラップが事実上の標準テクニック。
アンチパターン: グローバル変数でステータスを記録する
var lastStatus int func WriteHeader(code int) { lastStatus = code // 競合状態! }なぜNG: 全リクエスト goroutine が共有するグローバルだから、同時リクエスト時にステータスが混ざる。
修正: リクエストごとに
statusRecorderを new する(上記コード)。
8. ResponseWriter ラップの落とし穴 - http.Hijacker と http.Flusher
// statusRecorder は http.Hijacker を実装していないため WebSocket で壊れる
func (s *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := s.ResponseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, errors.New("not a Hijacker")
}
// Flush は SSE / ストリーミングで使う
func (s *statusRecorder) Flush() {
if f, ok := s.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}ラップすると失われるインターフェース
Go の
http.ResponseWriterは実は 複数のインターフェースを兼ね備えた具体型 が標準ライブラリから渡される:
http.Flusher- レスポンスを即時送信(SSE で必要)http.Hijacker- 下層の net.Conn を奪う(WebSocket で必要)http.CloseNotifier- クライアント切断通知(deprecated、今は context 経由)
statusRecorderでラップすると、http.Hijackerへの型アサーションが失敗して WebSocket が動かなくなる。実例: chi, echo の古いミドルウェアでも踏んだバグ。対策: ラッパー側でも
Hijack()、Flush()を実装して 下層に委譲する。これを徹底しないと、ミドルウェアを通したエンドポイントで突然 WebSocket が壊れる。
9. タイムアウトミドルウェア
func Timeout(d time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, d, `{"error":"request timeout"}`)
}
}http.TimeoutHandler は標準ライブラリ提供
標準で
http.TimeoutHandler(handler, timeout, message)が用意されている。指定時間内にハンドラが終わらなければ、自動で503 Service Unavailableを返す。ただし注意点:
- 内部のハンドラは止まらない(goroutine は走り続ける)。「クライアントへのレスポンス送信を諦める」だけ
- 真に止めたければ
r.Context()を ハンドラ内で監視 する(context が cancel される)
// ハンドラ側の正しい書き方
func slowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
resultChan := make(chan string, 1)
go func() {
// 重い処理
resultChan <- doExpensiveWork(ctx)
}()
select {
case result := <-resultChan:
w.Write([]byte(result))
case <-ctx.Done():
// 親 context が cancel された(タイムアウト or クライアント切断)
slog.Warn("request cancelled", "err", ctx.Err())
return
}
}10. ミドルウェアチェイン用のヘルパー
// 順番に重ねる(左から内側、右から外側、ではなく「書いた順に外側→内側」)
func Chain(mws ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
next = mws[i](next)
}
return next
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api", apiHandler)
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// 外側 → 内側の順に書く(直感的)
stack := Chain(
RequestID,
Recover,
Timeout(10*time.Second),
InjectLogger(logger),
)
http.ListenAndServe(":8080", stack(mux))
}Chain ヘルパーが無いとどうなる
// ネスト地獄 wrapped := RequestID(Recover(Timeout(10*time.Second)(InjectLogger(logger)(mux))))↑これは読めない。Chain を作るか、chi / echo のようなライブラリの
r.Use(mw)構文を使う。
11. chi のミドルウェア記法との比較
// chi の場合
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(10 * time.Second))
r.Use(middleware.Logger)
r.Get("/api", apiHandler)chi / echo / gin の Use との対比
方式 書き方 利点 標準 + Chain ヘルパー Chain(MW1, MW2)(handler)依存ゼロ chi の Use r.Use(mw)を順に呼ぶ直感的、サブルーターごとに切り替え可 gin の Use r.Use(mw)でgin.HandlerFuncgin 専用シグネチャ echo の Use e.Use(mw)でecho.MiddlewareFuncecho 専用シグネチャ 標準と chi は
http.Handler互換 なので、ミドルウェア資産が共通化できる。gin / echo は独自シグネチャなので相互運用不可。
12. 実装: 全部入りミドルウェアスタック
// middleware.go
package main
import (
"context"
"log/slog"
"net/http"
"runtime/debug"
"time"
"github.com/google/uuid"
)
type Middleware func(http.Handler) http.Handler
type ctxKey string
const (
requestIDKey ctxKey = "request_id"
)
// 1. リクエスト ID
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.NewString()
}
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 2. recover
func Recover(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
logger.Error("panic recovered",
"err", rec,
"stack", string(debug.Stack()),
"path", r.URL.Path,
)
http.Error(w, "internal server error", 500)
}
}()
next.ServeHTTP(w, r)
})
}
}
// 3. アクセスログ + ステータス記録
type statusRecorder struct {
http.ResponseWriter
status int
}
func (s *statusRecorder) WriteHeader(c int) {
s.status = c
s.ResponseWriter.WriteHeader(c)
}
func AccessLog(logger *slog.Logger) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rec := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rec, r)
rid, _ := r.Context().Value(requestIDKey).(string)
logger.Info("http",
"request_id", rid,
"method", r.Method,
"path", r.URL.Path,
"status", rec.status,
"duration_ms", time.Since(start).Milliseconds(),
"remote", r.RemoteAddr,
)
})
}
}
// 4. Chain ヘルパー
func Chain(mws ...Middleware) Middleware {
return func(next http.Handler) http.Handler {
for i := len(mws) - 1; i >= 0; i-- {
next = mws[i](next)
}
return next
}
}練習課題
- レート制限ミドルウェアを
golang.org/x/time/rateのrate.Limiterで実装 - 認証ミドルウェアを書き、
Authorization: Bearer XXXを検証して context にuser_idを積む slogを context から取り出すパターンを実装し、ハンドラ内でlog := LoggerFromCtx(r.Context())で使えるようにするhttp.TimeoutHandlerを使って/slowを3秒でタイムアウトさせる- WebSocket(
gorilla/websocket)を含むサーバーでstatusRecorderラップして Hijack が動かなくなることを再現 →Hijack()メソッド追加で修正 - ボーナス: chi の
middleware.Loggerのソースを読む → 業界水準の実装に触れる
締め: git で証跡を残す
cd ~/learn/go/level2/day04
git add .
git commit -m "feat(go-middleware): RequestID/Recover/AccessLogの合成可能ミドルウェア"
exitアンチパターン集 - やらかし事例
ミドルウェア設計の事故
1. 順序を間違えてログにステータスが残らない
Chain(AccessLog, Recover) // ← Recover が AccessLog より内側panic が AccessLog の
next.ServeHTTP内で起きると、AccessLog の後処理(ログ出力)に到達しない。2. context にロガー・DB を入れる
ctx = context.WithValue(ctx, dbKey, db) // ← NG公式ドキュメント明言: 「リクエストスコープのデータ以外を入れるな」。DB/Logger は依存注入で構造体フィールドに。
3. statusRecorder ラップで Hijack が動かなくなる WebSocket 用途で
http.Hijacker型アサーションが失敗。ラッパーにはHijack()Flush()も委譲実装が必要。4. panic 情報をクライアントに返す
http.Error(w, debug.Stack(), 500) // 攻撃者に地図を渡すスタックは ログに、クライアントには汎用エラーだけ。
5. グローバル変数で状態管理
var lastStatus int全 goroutine 共有でレース。リクエスト単位の
statusRecorderで。
対比表で違いを明確化
標準 vs chi vs gin/echo のミドルウェア記法
観点 標準 + Chain chi r.Usegin/echo Useシグネチャ func(http.Handler) http.Handler同左 独自型 net/http 互換 ◎ ◎ × サブパス適用 自前 r.Groupあり 移植性 高 高 低
ミドルウェア推奨順序
層 役割 外側/内側 RequestID ID 採番(後段ログ用) 最外 Recover panic 捕捉 + 構造化ログ 外側 AccessLog リクエスト/レスポンス記録 外側寄り Timeout 長すぎる処理を中断 中間 CORS プリフライト即返却 中間 RateLimit DoS 防御(認証前で節約) 中間 Auth Bearer Token 検証 内側 Authz 権限チェック 最内
自己評価チェックリスト
手を動かせた
-
Loggingミドルウェアを書いた -
Recoverで panic を握りつぶし、構造化ログに残した -
RequestIDミドルウェアで context に積み、ヘッダにも返した -
statusRecorderでラップして実ステータスをログ -
Chain(MW1, MW2, ...)ヘルパーを自作
説明できる
- ミドルウェアの型
func(http.Handler) http.Handlerを空で書ける - 推奨順序の理由(ID→ログ→recover→…)
- context.WithValue の許容範囲(リクエストスコープのみ)
-
HijackerFlusherインターフェース継承の必要性
やらかし回避
- panic 情報をクライアントに漏らさない
- context にロガー/DBを入れない
- グローバル変数で状態管理しない
- ラッパーでインターフェース継承を欠かさない
詰まった時のチートシート
| やりたいこと | コード |
|---|---|
| ミドルウェアの型 | func(http.Handler) http.Handler |
| 次を呼ぶ | next.ServeHTTP(w, r) |
| 早期 return(後続を呼ばない) | next.ServeHTTP を呼ばずに return |
| 値を context に積む | r = r.WithContext(context.WithValue(r.Context(), key, val)) |
| 値を取り出す | r.Context().Value(key) |
| タイムアウト | http.TimeoutHandler(next, d, msg) |
| パニック復帰 | defer func() { if rec := recover(); rec != nil {...} }() |
| ステータス記録 | statusRecorder で ResponseWriter をラップ |
| chi で連鎖 | r.Use(mw1); r.Use(mw2) |
「実務OK」基準
- 3-5個のミドルウェアを合成して動くサーバーが書ける
- recover で panic を握りつぶしつつ、内部情報を漏らさずクライアントに 500 を返せる
- context にリクエスト ID を積み、ログから一連の処理を grep できる
statusRecorderで実際のレスポンスコードをログに残せる- Hijacker / Flusher のインターフェース継承を意識した正しいラップが書ける
さらに深掘りするなら
- chi のミドルウェア集: github.com/go-chi/chi/tree/master/middleware - 業界標準実装の宝庫
- 公式: The Go Blog - Context and Cancellation
- Justinas Stankevičius: “Making and Using HTTP Middleware” - ミドルウェアパターンの古典解説
- 標準ライブラリ:
src/net/http/server.goのserverHandler.ServeHTTP- 標準内部の recover を読む - 書籍: 『Let’s Go』(Alex Edwards) - 中盤がミドルウェア構築の実践
次のレッスン
2-5 エラーハンドリング で Go 流のエラー設計(値としての error、%w ラップ、errors.Is/As、panic と recover の境界)を本気で扱う。エラーは設計の半分。
つながりの予告
- 本章の
Recoverミドルウェアは 2-5_エラー処理 の panic vs error 境界設計に直結 context.WithValueで積んだリクエスト ID は 3-6_context でキャンセル・タイムアウトと組み合わせるAuthミドルウェアは 3-5_認証 で本格実装