🔩 ねじき教室 Go と Web の教室

ミドルウェア

読了目安 約3分

ハンドラを受け取ってハンドラを返す関数というパターンで、全ハンドラ共通の処理を 1 箇所に書く方法を学びます。

この章の目次

すべてのリクエストについて、どのパスに何秒かかったかをログに残したいとします。 各ハンドラに計測コードを書くと、ハンドラが 30 個なら 30 回書き、形式を変えるときも 30 箇所を直すことになります。 この「全ハンドラに共通する処理を 1 箇所に書く場所」がミドルウェアです。

ハンドラを受け取ってハンドラを返す関数

ルーティングとハンドラで見たとおり、サーバーはリクエストのたびに、渡された 1 つの http.Handler に処理を丸投げします。 そこで、本来のハンドラを包んで「前後に仕事を足した新しいハンドラ」を作り、サーバーにはそちらを渡します。

次の内容を main.go に保存してください。

Go
package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func withLogging(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("GET /hello", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello!")
	})

	mux.HandleFunc("GET /slow", func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(2 * time.Second)
		fmt.Fprintln(w, "お待たせしました")
	})

	log.Fatal(http.ListenAndServe(":8080", withLogging(mux)))
}

go run main.go で起動し、別のターミナルから両方のパスを叩きます。

シェル
curl http://localhost:8080/hello
curl http://localhost:8080/slow

サーバー側のターミナルに、次のようなログが出ます。

テキスト
2026/07/17 10:00:01 GET /hello (312.5µs)
2026/07/17 10:00:05 GET /slow (2.001s)

どちらのハンドラにも計測のコードはないのに、両方が記録されました。

仕組みを読む

withLogging は、ハンドラを受け取ってハンドラを返す関数です。 返すのは「時刻を控え、預かった next を呼び、経過時間をログに書く」新しいハンドラで、関数のハンドラ化には前々章のアダプタ http.HandlerFunc を使っています。

効かせ方は最後の 1 行で、ListenAndServe に渡すものを mux から withLogging(mux) に差し替えました。 ServeMux もハンドラの一種なので、まるごと包めます。 入口が「ログを書いてから mux に渡す」ものに変わり、mux の先の全ハンドラに計測が効きます。

前後に挟めるという構造

リクエストの通り道は次のとおりで、ハンドラの前後の両方に処理を挟めます。

テキスト
リクエスト
    │
    ▼
┌─ withLogging ─────────────┐
│  前処理: 開始時刻を控える     │
│      │                    │
│      ▼                    │
│  next.ServeHTTP           │
│  (ServeMux → 各ハンドラ)   │
│      │                    │
│      ▼                    │
│  後処理: 経過時間をログに書く  │
└───────────────────────────┘
    │
    ▼
レスポンス

next.ServeHTTP(w, r) より前のコードはハンドラの実行前に、後のコードは実行後に動きます。 next を呼ばない選択もできます。 たとえば認証のミドルウェアは、正しい認証情報がなければ 401 Unauthorized を返して return し、本来のハンドラには触らせません。

ミドルウェアは重ねられます。 withAuth(withLogging(mux)) と包めば、リクエストは外側から順に認証、ログ、ハンドラと通過します。 関数合成と同じ要領です。

何をミドルウェアに置くか

向いているのは、「どのハンドラか」に依存しない処理です。

  • アクセスログ
  • 認証の確認
  • パニック回復(ハンドラ内の panic を捕まえ、整った 500 レスポンスを返す)

特定のパスだけの都合は、そのハンドラ自身に書きます。

ISUCON の第一歩「どのエンドポイントが遅いか」の計測も、この形のアクセスログで仕込むのが定番です。 ログを集計する手順は推測するな、計測せよで扱います。 次章のフレームワークにも、これらのミドルウェアが既製品として揃っています。