Go実践 #2 ルーティング — Go 1.22+ ServeMux
#1 はじめてのHTTPサーバーでは単一の/だけを処理しました。実戦のサーバーは — 複数のパスとメソッドを扱います。
Go 1.22 (2024)以前は — 標準のServeMuxがメソッドマッチングやパスパラメータをサポートしておらず、外部ルーターがほぼ必須でした。1.22から — 標準だけで通常は十分です。
Go 1.22+ 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ヘッダーによって異なるハンドラ。マルチドメインサービスに有用。
マッチングの優先順位 #
複数のパターンがマッチする場合:
- より具体的なパターンが優先(ホスト明示 > 未明示、正確なセグメント > wildcardなど)
- 同じ優先順位なら — panic(登録時に衝突を検出)
以前のServeMuxの曖昧さが — 今回のバージョンで明確なルールに整理されました。
よくあるパターン — /で終わるパス
#
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処理 #
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
#
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が近づきました。
大きなフレームワーク — gin、echo、fiber
#
ルーター + ミドルウェア + レスポンスヘルパーなどが束になったフレームワーク。
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/
#
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で安全に入出力すること、デコードエラー処理、入力検証パターンを整理します。