フューチャー技術ブログ

Go1.22 リリース連載 HTTPルーティングの強化

はじめに

Go1.22リリース連載 の5本目です。
本記事ではGoの標準ライブラリである net/http の ServeMux におけるルーティング周りの強化について取り上げます。

関連する Release Note と Issue はこちらを参照してください。

https://tip.golang.org/doc/go1.22#enhanced_routing_patterns

https://github.com/golang/go/issues/61410

変更点

HTTPメソッドの指定が可能に

ServeMux.Handle や ServeMux.HandleFunc を使用してハンドラを登録する際に GET /xxx のようにHTTPメソッド指定して、ハンドラを呼び分けることができるようになりました。

mux := http.NewServeMux()
// GETを指定したハンドラの登録
mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World")
})

従来はハンドラの中で、自前でHTTPメソッドによって処理を呼び分ける(もしくは chi のようなHTTPメソッドの呼び分けに対応したルーティングライブラリを利用する)必要がありました。

mux := http.NewServeMux()
// 従来はハンドラを登録する際にHTTPメソッドを指定できない
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
fmt.Fprintf(w, "Hello World")
}
})

HTTPメソッドの指定について、3点ほど詳細を補足しておきます。

  • メソッド指定のパターン(GET /hello)とメソッドを指定しないパターン(/hello)の両方が登録された場合は、メソッド指定のハンドラが優先されます。
  • ハンドラをGETメソッドで登録した場合は、自動的にHEADメソッドでも登録されます。
  • 存在しない(標準仕様として定められていない)HTTPメソッドを指定した場合でも特に起動時のエラーは発生しません。(ちなみにパスに "HOGE /" を指定した場合 curl -X HOGE ... で呼び出すことが可能です。)

ワイルドカードの指定が可能に

HTTPメソッドが指定できるようになっただけでなく、/items/{id} のようにワイルドカードが使用できるようになりました。ワイルドカードにマッチしたパスセグメントの値については Request.PathValue でアクセスできます。

mux := http.NewServeMux()
// GET /items/1234 -> 1234
// GET /items/1234/0 -> 404 page not found
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", r.PathValue("id"))
})

また、/files/{path...} のように ... で終わるワイルドカードを指定することで、特定のパスに続く全てのセグメントにマッチさせることができます。

mux := http.NewServeMux()
// GET /files/hoge -> hoge
// GET /files/hoge/fuga -> hoge/fuga
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s", r.PathValue("path"))
})

従来は /items/{id} のようなパスパラメータに対するワイルドカードマッチを実現するためには、/items/ のように末尾スラッシュをつけることで、/items/で始まる全てのリクエストをハンドリングし、ハンドラの中で、自前でパスパラメータをパースする必要がありました。

mux := http.NewServeMux()
mux.HandleFunc("/items/", func(w http.ResponseWriter, r *http.Request) {
// フルパス(URL.Path)から取得したいパスパラメータをパースして取得する必要がある
id := r.URL.Path[len("/items/"):]
fmt.Fprintf(w, "%s", id)
})

Go1.22においても末尾にスラッシュを指定した場合の挙動(指定したパスで始まる全てのエンドポイントがハンドリングされる挙動)は従来と変わりませんが、末尾スラッシュで終わるパスのみに完全にマッチさせたい場合は /items/{$} のように {$} を付与することで完全なパターンマッチを実現できます。

mux := http.NewServeMux()
// GET /items/ -> Matched!
// GET /items/1234 -> Matched!
mux.HandleFunc("GET /items/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Matched!")
})
// GET /items/ -> Matched!
// GET /items/1234 -> 404 page not found
mux.HandleFunc("GET /items/{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Matched!")
})

後方互換性について

https://pkg.go.dev/net/http@go1.22rc2#hdr-Compatibility

ほとんどのユースケースにおいて問題になることはないと思いますが、パスに {} を使用していたケースなどでは問題が起きるかもしれません。

例えば、ワイルドカードが存在しなかった従来のバージョンでは下記のように /items/{hoge}items/{fuga} は別々のパスとして認識され、異なるハンドラを登録し、呼び分けることができました。

mux := http.NewServeMux()
mux.HandleFunc("/items/{hoge}", func(w http.ResponseWriter, r *http.Request) {
// (略)
})
mux.HandleFunc("/items/{fuga}", func(w http.ResponseWriter, r *http.Request) {
// (略)
})

しかしながらGo1.22では {} で囲んだ部分はワイルドカードとなるため、/items/{hoge}items/{fuga} は同一のパターンと認識され、起動時に panic となります。

panic: pattern "/items/{fuga}" (registered at xxxxx) conflicts with pattern "/items/{hoge}" (registered at xxxxx):
/items/{fuga} matches the same requests as /items/{hoge}

もしワイルドカードなどの新しい仕様が許容できない場合は、環境変数に GODEBUG=httpmuxgo121=1 を設定することで、Go1.22を使いつつ以前の動作のまま動かすことが可能です。

性能計測

Go標準のベンチマーク機能を用いて ServeMux とルーティング機能を提供する主要ライブラリである chiGorilla の性能を比較してみます。

シナリオ

各ライブラリを利用して以下の4パターンのAPIを定義し、それぞれ ServeHTTP をテストする形で性能を計測します。

  • パスパラメータが存在しないケース
  • パスパラメータが1個存在するケース
  • パスパラメータが5個存在するケース
  • パスパラメータが10個存在するケース

ソースコードはこちらで公開してますが、各ハンドラ内ではパスパラメータ値の取得まで行っております。

実行結果

実行結果に表示されている各ケースの命名については、Benchmark${ライブラリ名}${パスパラメータ数} (例. BenchmarkServeMux0)となります。

結果を見ると、パスパラメータが存在しない or 1個のみの場合は ServeMux > chi > Gorilla となり、パスパラメータが5個、10個と増えた場合は chi > ServeMux > Gorilla となっています。

$ go test -bench . -benchmem
goos: linux
goarch: amd64
pkg: github.com/rhumie/go-1.22-feature
cpu: Intel(R) Core(TM) i7-8569U CPU @ 2.80GHz
BenchmarkServeMux0-4 18774772 53.84 ns/op 0 B/op 0 allocs/op
BenchmarkServeMux1-4 7989613 139.1 ns/op 16 B/op 1 allocs/op
BenchmarkServeMux5-4 2139224 562.3 ns/op 240 B/op 4 allocs/op
BenchmarkServeMux10-4 1147263 1054 ns/op 496 B/op 5 allocs/op
BenchmarkChi0-4 4200387 279.1 ns/op 336 B/op 2 allocs/op
BenchmarkChi1-4 3440340 328.7 ns/op 336 B/op 2 allocs/op
BenchmarkChi5-4 2231761 530.8 ns/op 336 B/op 2 allocs/op
BenchmarkChi10-4 1559656 782.3 ns/op 336 B/op 2 allocs/op
BenchmarkGorilla0-4 1668428 731.7 ns/op 784 B/op 7 allocs/op
BenchmarkGorilla1-4 900196 1251 ns/op 1088 B/op 8 allocs/op
BenchmarkGorilla5-4 373233 2799 ns/op 1152 B/op 8 allocs/op
BenchmarkGorilla10-4 161706 6421 ns/op 1814 B/op 9 allocs/op
PASS

一般的にAPIのパスパラメータはそこまで多くならない(通常は0 ~ 2個)よう設計されることが多いと思います。
ルーティングライブラリを導入せず、標準の ServeMux だけで十分というケースが増えそうですね。