Go実践 #2 ルーティング — Go 1.22+ ServeMux

読了 5分

#1 はじめてのHTTPサーバーでは単一の/だけを処理しました。実戦のサーバーは — 複数のパスとメソッドを扱います。

Go 1.22 (2024)以前は — 標準のServeMuxがメソッドマッチングやパスパラメータをサポートしておらず、外部ルーターがほぼ必須でした。1.22から — 標準だけで通常は十分です。

Go 1.22+ ServeMux #

新しいServeMux
mux := http.NewServeMux()

mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)

http.ListenAndServe(":8080", mux)
  • メソッド + パス"GET /users"
  • パスパラメータ{id}の形で

以前は — すべてif r.Method == "GET"の分岐を手書きしていた部分。1.22から1行で。

パスパラメータの読み取り #

パスパラメータ
func getUser(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	// ...
}

r.PathValue("id") — その位置の値をstringで。整数が必要なら直接strconv.Atoi

Wildcardと... #

ワイルドカード
mux.HandleFunc("GET /files/{path...}", serveFile)
  • {name} — 1セグメント
  • {name...} — 末尾までのすべてのセグメント
// /files/a/b/c.txt → r.PathValue("path") == "a/b/c.txt"

Hostマッチング #

ホスト別ルーティング
mux.HandleFunc("api.example.com/health", apiHealth)
mux.HandleFunc("admin.example.com/health", adminHealth)

同じパスでも — Hostヘッダーによって異なるハンドラ。マルチドメインサービスに有用。

マッチングの優先順位 #

複数のパターンがマッチする場合:

  1. より具体的なパターンが優先(ホスト明示 > 未明示、正確なセグメント > wildcardなど)
  2. 同じ優先順位なら — panic(登録時に衝突を検出)

以前のServeMuxの曖昧さが — 今回のバージョンで明確なルールに整理されました。

よくあるパターン — /で終わるパス #

prefixマッチング vs 完全一致
mux.HandleFunc("GET /api/", apiHandler)        // /api/* すべて(prefix)
mux.HandleFunc("GET /api/{$}", apiRoot)        // /api/ 完全一致
mux.HandleFunc("GET /api", redirectToSlash)    // /api 完全一致
  • /api//api/...のすべてのパス(prefix)
  • /api/{$}/api/のみ(完全一致)
  • /api/apiのみ(slashなしの形)

この分離も1.22の新機能。以前はprefixと完全一致が混ざってややこしかったです。

グループ/prefix処理 #

prefix別のmux
api := http.NewServeMux()
api.HandleFunc("GET /users", listUsers)
api.HandleFunc("GET /posts", listPosts)

root := http.NewServeMux()
root.Handle("/api/", http.StripPrefix("/api", api))

別のmuxを作って — StripPrefixでprefixを除去。API v1、v2の分離に向いています。

/api/v1のようなバージョンprefix #

v1 := http.NewServeMux()
v1.HandleFunc("GET /users", listUsers)

v2 := http.NewServeMux()
v2.HandleFunc("GET /users", listUsersV2)

root := http.NewServeMux()
root.Handle("/api/v1/", http.StripPrefix("/api/v1", v1))
root.Handle("/api/v2/", http.StripPrefix("/api/v2", v2))

同じハンドラを持つ2つのmuxを — prefixごとに登録。

外部ルーターが似合う場面 #

標準が十分になった1.22以降でも — 外部ルーターが依然として意味を持つ場面があります。

go-chi/chi #

chiの例
import "github.com/go-chi/chi/v5"

r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)

r.Route("/users", func(r chi.Router) {
	r.Get("/", listUsers)
	r.Post("/", createUser)
	r.Route("/{id}", func(r chi.Router) {
		r.Use(loadUser)
		r.Get("/", getUser)
		r.Delete("/", deleteUser)
	})
})

http.ListenAndServe(":8080", r)

利点:

  • グループ単位でミドルウェア適用
  • ネストルーターRoute / Mount
  • 標準net/http互換 — chiのルーターもhttp.Handler

大きなAPIで — ルートツリーが見やすく、グループ別ミドルウェアが自然。標準ServeMuxだけでは — ミドルウェアをルート単位で挟むのがやや煩雑です。

gorilla/mux #

古くからある標準的なルーター。Go 1.22以前の事実上のデフォルトでしたが — 今はchiの方が活発で、標準muxが近づきました。

大きなフレームワーク — ginechofiber #

ルーター + ミドルウェア + レスポンスヘルパーなどが束になったフレームワーク。

ginの例
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
	c.JSON(200, gin.H{"id": c.Param("id")})
})

利点:

  • レスポンスヘルパー(JSON、render)が豊富
  • ベンチマークが速い(特にfiberはfasthttpベース)
  • 開発スピードが速い

trade-off:

  • 標準インターフェースから離れる — 他のライブラリと組み合わせるのがやや煩雑
  • ラーニングカーブ — フレームワーク別のコンベンション
  • fasthttpベースはnet/httpと互換なし

主観: Goのデザイン哲学(小さな標準 + 明示的)とは — chiが一番よく合う傾向。gin/echoは他言語/フレームワークに慣れた方に親しみやすい。

どちらを選ぶか? #

状況おすすめ
小さなサービス、ルート < 30個標準ServeMux
グループ/ネストが多く — ルート単位ミドルウェアが頻繁chi
速い開発/プロトタイプ、レスポンスヘルパー豊富を好むgin / echo
極限の性能(特殊な場面)fiber (fasthttp)

基本は標準から始めて → 詰まる場面が見えたらchi。fasthttpベースは — 標準net/http互換でなくライブラリの選択肢が狭くなる点に注意。

ルートのテスト #

net/http/httptestで — 外部依存なくローカルで。

ルートのテスト
func TestGetUser(t *testing.T) {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /users/{id}", getUser)

	req := httptest.NewRequest("GET", "/users/42", nil)
	w := httptest.NewRecorder()

	mux.ServeHTTP(w, req)

	if w.Code != http.StatusOK {
		t.Errorf("expected 200, got %d", w.Code)
	}
}

#6 テストで詳しく。

よくある落とし穴 #

1) 登録の衝突 → panic #

よくある落とし穴
mux.HandleFunc("GET /a/{id}", h1)
mux.HandleFunc("GET /a/{name}", h2)    // ✗ 同じ優先順位の衝突 → panic

{id}{name}は同じ位置。名前が違うワイルドカードは同一のパターン。コンパイルエラーではなく起動時にpanic

2) /api vs /api/ #

2つのパターンは別物
mux.HandleFunc("GET /api", a)     // /api のみ
mux.HandleFunc("GET /api/", b)    // /api/、/api/anything

リクエストが/apiで来ても — 自動redirectが起きて/api/へ行くことがあります。クライアントがredirectに従わないとレスポンスを受け取れない場面です。

3) メソッドマッチングをしないケース #

mux.HandleFunc("/users", h)

メソッドを明示しないと — すべてのメソッドがマッチ。POSTだけ受けたいなら明示的に"POST /users"

Method-not-allowed #

特定のパスにGETだけ登録されていてPOSTが来ると — 標準ServeMuxは405 Method Not Allowedを自動で返します(1.22+)。以前のバージョンでは404でした。小さな変化ですが — REST APIに適切なレスポンス。

まとめ #

この記事で整理した内容:

  • Go 1.22+ ServeMux — メソッド + パスパラメータ + ホストマッチング
  • r.PathValue("id")でパラメータを読む
  • {...}ワイルドカード、{$}完全一致
  • prefix別mux + StripPrefixでグループ化
  • chi — グループ別ミドルウェアが頻繁なら
  • gin / echo / fiber — レスポンスヘルパーや性能が決め手のとき
  • 登録の衝突は起動時にpanic
  • /api/api/は別のパターン

次の記事(#3 JSON入出力)では — encoding/jsonで安全に入出力すること、デコードエラー処理、入力検証パターンを整理します。

X