CodeGym /コース /Go SELF /Go 1.22+ における method と wildcard を使った ServeMux パターン

Go 1.22+ における method と wildcard を使った ServeMux パターン

Go SELF
レベル 60, レッスン 0
使用可能

1. なぜルーティングを改善するのか: if が少ないほど楽になる

HTTP サーバーを書き始めたばかりの頃は、1 つの handler を作って、その中で「ミニディスパッチャ」のようなものを動かすのが自然に思えます。つまり、r.URL.Path を確認し、r.Method を確認し、パス文字列を分割し、strings.Split を使い、そしてスラッシュを見落としていないことを祈る、というやり方です。これは動きます……でも、ルートが 3 つを超えたあたりから、チーム(あるいは 1 週間後の自分)が「なぜ POST /tasks がタスク表示の handler に入るんだ?」と首をかしげるようになります。

Go 1.22 で標準ライブラリはとても気持ちのよい一歩を踏み出しました。net/http.ServeMux に メソッド付きパターンと wildcard が追加されたのです。つまり、GET /tasks や POST /tasks のように、handler のコード内ではなくルーティング層でルートを直接記述できます。同時に、{id} や {path...} のような wildcard セグメント(パスの一部を捕まえるもの)も使えるようになりました。Go 1.22 のリリースノートでも、net/http.ServeMux が使う patterns にメソッドと wildcard セグメントが追加されたことが、GET /task/{id}/ のような例とともに明記されています。

私たち、つまりこれから学ぶ側にとっての最大の利点は、文字列操作の「魔術」が減って、宣言的な書き方が増えることです。サーバーの可読性が上がり、テストもしやすくなり、「あ、メソッドのチェック忘れてた」みたいなミスもかなり減ります。

2. http.ServeMux をリクエストのディスパッチャとして見る

サーバーを宅配オフィスだと想像してください。リクエストは紙を持った配達員です。「メソッド: GET、パス: /api/v1/tasks/10」。ServeMux は入口の受付のようなもので、その紙を見て、どの担当者に配達員を回すべきかを判断します。受付がもたつくと、みんなが列に並んで文句を言い始めます。

ServeMux の仕事はシンプルです。mux.Handle(...) や mux.HandleFunc(...) で登録した pattern にリクエストを対応付けることです。以前の pattern は主に「パス」だけに関するもので、開発者は /tasks という 1 つの pattern を登録し、その後 handler 内で switch r.Method を書くことがよくありました。これは動くやり方でしたが、コピペと細かなミスを生みやすい方法でもありました。

今では、メソッドをルートの一部として定義できます。つまり、受付は「どこに行くか」だけでなく(/tasks)、「何をしに行くか」も見ます(GET/POST)。これこそ、ちゃんとした API に求めるものです。

イメージを固めるために、リクエストが mux を通る流れを図にするとこうなります:

flowchart TD
    A[クライアントがリクエストを送る] --> B[http.Server]
    B --> C[ServeMux: 適切な pattern を探す]
    C -->|見つかった| D[Handler]
    C -->|見つからない| E[状況に応じて 404 / 405]
    D --> F[クライアントへ応答]

重要なのは、私たちは今も外部ルーターやフレームワークの「魔法」なしで、標準の net/http の世界にいるということです。ServeMux は同じ ServeMux のままですが、より賢くなりました。

3. Method-aware patterns: GET /tasks で r.Method をチェックしない

"GET /tasks" のような文字列を見ると、「フレームワークっぽい妙な構文かな?」と感じるかもしれません。でも実際にはとても素直な考え方です。ServeMux は文字列 pattern を受け取り、Go 1.22 以降はその pattern に HTTP メソッドを含められます。

最もよくある実践上の利点は、各 handler で毎回同じことを書かなくて済むようになることです:

if r.Method != http.MethodGet {
    w.WriteHeader(http.StatusMethodNotAllowed)
    return
}

その代わりに、正しい route を登録するだけで済みます。Handler はより短く、よりまっすぐになります。「1 つのメソッドに対して 1 つの動作をする」と明確になるのです。

最小例: GET /health

package main

import (
	"fmt"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusOK)
		fmt.Fprintln(w, "ok") // ok
	})

	_ = http.ListenAndServe(":8080", mux)
}

書き方に注目してください。メソッドとパスの間には必ずスペースが必要です。これは「スタイル」ではなく、pattern 構文の一部です。

1 つの path に 2 つの method: GET /tasks と POST /tasks

API では定番です。GET は一覧を読む、POST は作成する。

package main

import (
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte("list tasks\n"))
	})

	mux.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(http.StatusCreated)
	})

	_ = http.ListenAndServe(":8080", mux)
}

ここで大事なのは、1 つの path に複数の route があるということです。これは普通です。受付役(mux)は、ただ異なる「routing rule」を見ているだけです。

なぜこれが「Go らしい」のか

Go は、インフラ的なルールを 1 か所 で表したがる言語です。handler にメソッドチェックを散らすと、小さな不整合がたくさん生まれます。すると誰かが PUT を追加し、Allow を書き忘れ、どこかでは 405 ではなく 400 を返してしまい、カオスになります。

すでに「handler をアダプタで包む」考え方を見たことがあるなら、それも同じ哲学です。Go チームの古い資料には、自分の型 appHandler(関数型)を作り、ServeHTTP を実装して、エラーを一元的に処理する例があります。そこで明示されているのは、receiver は関数でもよく、それによってインフラを 1 か所に保ちやすくなる、ということです。

method-aware patterns も同じ効果があります。インフラの一部が handler 本体から route 登録の場所へ移動するのです。

4. ServeMux の wildcard: {id} と {path...}

method-aware patterns が r.Method に関する悩みを減らすなら、wildcard はパスに対する文字列操作の悩みを減らしてくれます。しかも、パスの文字列操作というのは、「慎重にやっているつもり」から始まって、突然二重スラッシュや空セグメント、予想外の末尾で壊れる類のバグです。

{id}: パスの 1 セグメント

次のような pattern:

  • GET /tasks/{id}

は、/tasks/ の後に ちょうど 1 つのセグメント が来ることを意味します。たとえば /tasks/12 や /tasks/abc です。この段階で大切なのはマッチングだけです。ルートが存在し、リクエストが handler に「入る」ということです。

{id} の意味は、「ここは可変部分の path です」ということです。まだ値をどう取り出すかは扱いませんが(それは別の話です)、可変セグメントがあるだけで、手動の strings.Split は不要になります。

{path...}: 複数セグメントにわたる path の末尾

ときには 1 セグメントではなく、「その先すべて」をマッチしたいことがあります。典型例は静的ファイル配信です(API ではやや少ないですが、学習用の例としてはとても良いです)。

次のような pattern:

  • GET /assets/{path...}

は、/assets/ の後に logo.png でも、css/app.css でも、images/icons/edit.svg でもよく、これらをすべて「末尾」としてまとめてマッチします。URL の「ディレクトリ」全体を 1 つの handler で扱いたいときに便利です。

実践でなぜ必要なのか

リソースを持つ API を作り始めると、ほぼ必然的に次のような構成になります。

  • 一覧: GET /tasks
  • 特定のエンティティの操作: GET /tasks/{id}
  • 削除: DELETE /tasks/{id}

そして wildcard を使わないと、かなりの確率で「r.URL.Path を手でパースしよう」となります。これは、森の中の細い道に入り込んで、すぐに罠を踏みやすいのと同じです。

ServeMux はどうやって最適な route を選ぶのか

route が増えると、自然にこんな疑問が出ます。複数の pattern が「合っている」場合、誰が勝つのか?

乾いたルール列挙(呪文のように読まれがちなもの)ではなく、日常的なロジックで考えてみましょう。受付に次の 2 つの rule があるとします:

  • 「/assets/… へのすべてのリクエストを静的ファイル担当へ送る」
  • 「/assets/logo.png へのすべてのリクエストをデザイナーへ送る。彼はこのファイルが大好きだ」

この場合、より具体的な rule が勝つべきです。

ServeMux でも考え方は同じです。より specific な pattern が勝ちます。そして method-aware patterns は、さらに 1 段階 specific になります。つまり、GET /tasks は、すべてのメソッド向けの単なる /tasks よりも具体的です。なので、両方を登録した場合、GET /tasks のリクエストは「GET 側」に送るのが自然です。

初心者が覚えておくとよい別の点があります。query パラメータ(? の後ろ)は handler の選択に関与しません。つまり、/tasks?done=true も結局は /tasks としてマッチします。もし "GET /tasks?done=true" のような pattern を書こうとすると、handler に入れず、404 を見ながら「でも、全部正しいはずなのに!」となってしまいます。

5. HTTP アプリケーションの骨組み: route を登録する

今の目標は、タスク保存機能を実装することでも CRUD を完成させることでもありません(それは別の話です)。routing の骨組みを作り、どんな route があるのか、メソッド+パスがどう具体的な handler になるのかを見えるようにすることです。

最小のサーバーを作ります。以下を行います:

  • GET /health に対して ok を返す
  • GET /api/v1/tasks と POST /api/v1/tasks を区別できる
  • /api/v1/tasks/{id} で task を id によって扱う GET と DELETE の route を持つ
  • /assets/{path...} に対して「末尾 wildcard」の例を示す
package main

import (
	"fmt"
	"net/http"
)

func main() {
	mux := http.NewServeMux()

	mux.HandleFunc("GET /health", health)

	mux.HandleFunc("GET /api/v1/tasks", listTasks)
	mux.HandleFunc("POST /api/v1/tasks", createTask)

	mux.HandleFunc("GET /api/v1/tasks/{id}", getTask)
	mux.HandleFunc("DELETE /api/v1/tasks/{id}", deleteTask)

	mux.HandleFunc("GET /assets/{path...}", assets)

	_ = http.ListenAndServe(":8080", mux)
}

func health(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok") // ok
}

func listTasks(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "list tasks") // タスク一覧
}

func createTask(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusCreated)
	fmt.Fprintln(w, "created") // 作成済み
}

func getTask(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "get task by id") // id で task を取得
}

func deleteTask(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusNoContent)
}

func assets(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "static asset") // 静的アセット
}

このコードで重要なこと:

  • handler 内で r.Method を 確認しない。ServeMux がすでにメソッドで handler を選んでくれているからです。
  • r.URL.Path を 解析しない。route を登録しただけです。もちろん、handler getTask はまだ、どの {id} が来たのかを知りませんが、少なくとも /api/v1/tasks/{id} に対してのみ呼ばれることは保証されています。これは大きな利点です。
  • mux := http.NewServeMux() を明示的に作り、それを ListenAndServe に渡しています。これによって、誤って DefaultServeMux に route を登録してしまい、別の mux でサーバーを起動してしまう、という事故を減らせます(典型的なバグ: 「なんで全部 404 なんだ?」)。

コードの感触はどう変わるか

高尚な言い方をしないなら、method-aware patterns と wildcard はサーバーをより「行ベース」ではなく、より「構造ベース」にしてくれます。

route 登録のブロックを見ると、ほとんど API の仕様書のように見えます。何をサポートしているのかを示す小さな一覧表のようなものです。そして将来、たとえば「route がない」のか「route はあるが method が合わない」のかを正しく区別したくなったとき、pattern に method があることで、その振る舞いはずっと自然になります。

加えて、純粋に心理的にも、手書きの if が減り、ミスしやすい箇所も減ります。これは特に初心者にとって重要です。なぜなら、脳はすでに := と = を混同しないことや、import を忘れないことに精一杯だからです。

6. ServeMux patterns でよくあるミス

ミス 1: method と path の間のスペースを忘れる。
pattern 文字列は「たぶん何とか解釈される」ものではなく、明確な構文です。"GET /api/v1/tasks" の代わりに "GET/api/v1/tasks" と書くと、まったくマッチしない route になり、ネットワークが壊れたのかと疑うまで 404 を見続けることになります。

ミス 2: query パラメータを pattern に入れようとする。
"/tasks?done=true" は、意味として pattern では動きません。query は path の一部ではないからです。正しい pattern は "GET /tasks" で、フィルタリングは r.URL.Query() を読む別のロジックです(そしてそれは handler の選択に影響すべきではありません)。

ミス 3: 理由もなく 2 つのスタイルを混ぜる。つまり method-aware patterns と、handler 内の switch r.Method を両方使う。
「念のため」にこうすることがありますが、その後で両方の場所を更新し忘れます。結果として、ServeMux はすでに method を保証しているのに、handler が古いチェックのせいで 405 を返すことがあります。つまり「自分で自分の敵」みたいな挙動です。特定のコード領域では 1 つのスタイルに絞りましょう。pattern に method があるなら、handler では通常 method を再チェックしません。

ミス 4: r.URL.Path を、{id} があるのに手で解析しようとする。
これは他の言語や昔の実験から来た習慣です。問題は、path の手動パースはすぐに条件分岐の塊になり、テストしにくく壊しやすいことです。ServeMux が wildcard を理解できるなら、そちらに任せた方がよいです。スラッシュ当てゲームで競う必要はありません。

ミス 5: route を DefaultServeMux に登録したのに、サーバーは別の mux で起動している。
http.HandleFunc(...) を使うと DefaultServeMux を操作しています。mux := http.NewServeMux() を作って mux.HandleFunc(...) と書くと、特定の mux を操作していることになります。この 2 つを混同すると、route が「消えた」ように見えます。だから学習プロジェクトでは、常に明示的な mux を作り、常にそれを ListenAndServe に渡す、というルールを守るのがよいです。

1
タスク
Go SELF, レベル 60, レッスン 0
ロック未解除
サービスのヘルスチェック
サービスのヘルスチェック
1
タスク
Go SELF, レベル 60, レッスン 0
ロック未解除
タスクボード
タスクボード
1
タスク
Go SELF, レベル 60, レッスン 0
ロック未解除
ルートマップ
ルートマップ
1
タスク
Go SELF, レベル 60, レッスン 0
ロック未解除
優先順位のルール
優先順位のルール
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION