The Go Blog
Go 1.22 のルーティング強化
Go 1.22 では、net/http パッケージのルーターにメソッドマッチングとワイルドカードという2つの強化がもたらされます。これらの機能により、一般的なルーティングを Go コードではなくパターンとして表現できるようになります。説明や使用は簡単ですが、複数のリクエストに一致するパターンがある場合に、どのパターンを優先するかという適切なルールを考案するのは困難でした。
これらの変更は、Go を本番システム構築に優れた言語にするための継続的な取り組みの一環として行われました。私たちは多くのサードパーティ製 Web フレームワークを調査し、最も使用されていると思われる機能を抽出し、それらを net/http に統合しました。次に、GitHub の議論と提案 issueでコミュニティと協力することで、選択を検証し、設計を改善しました。これらの機能を標準ライブラリに追加することは、多くのプロジェクトにとって依存関係が1つ減ることを意味します。しかし、サードパーティ製 Web フレームワークは、現在のユーザーや高度なルーティングニーズを持つプログラムにとって引き続き優れた選択肢です。
機能強化
新しいルーティング機能は、net/http.ServeMux の2つのメソッド Handle と HandleFunc、および対応するトップレベル関数 http.Handle と http.HandleFunc に渡されるパターン文字列にほぼ専ら影響します。唯一の API 変更は、ワイルドカードマッチングを扱うための net/http.Request の2つの新しいメソッドです。
変更点を、すべての投稿が整数識別子を持つ架空のブログサーバーで説明します。GET /posts/234 のようなリクエストは、ID 234 の投稿を取得します。Go 1.22 より前では、これらのリクエストを処理するコードは次のような行から始まっていました。
http.HandleFunc("/posts/", handlePost)
末尾のスラッシュは、/posts/ で始まるすべてのリクエストを handlePost 関数にルーティングし、その関数は HTTP メソッドが GET であることを確認し、識別子を抽出し、投稿を取得する必要がありました。メソッドチェックはリクエストを満たすために厳密には必要ではないため、省略してしまうという自然な間違いが発生していました。そうなると、DELETE /posts/234 のようなリクエストで投稿がフェッチされることになり、少なくとも驚くべきことです。
Go 1.22 では、既存のコードは引き続き機能しますが、代わりに次のように記述できます。
http.HandleFunc("GET /posts/{id}", handlePost2)
このパターンは、パスが「/posts/」で始まり、2つのセグメントを持つ GET リクエストに一致します。(特別なケースとして、GET は HEAD にも一致します。その他のすべてのメソッドは完全に一致します。) handlePost2 関数はメソッドをチェックする必要がなくなり、識別子文字列の抽出は Request の新しい PathValue メソッドを使用して記述できます。
idString := req.PathValue("id")
handlePost2 の残りの部分は handlePost と同様に動作し、文字列識別子を整数に変換して投稿をフェッチします。
他の一致するパターンが登録されていない場合、DELETE /posts/234 のようなリクエストは失敗します。HTTP セマンティクスに従い、net/http サーバーは、このようなリクエストに対して、利用可能なメソッドを Allow ヘッダーにリストした 405 Method Not Allowed エラーで応答します。
ワイルドカードは、上記の例の {id} のようにセグメント全体に一致することも、... で終わる場合は、パターン /files/{pathname...} のようにパスの残りのすべてのセグメントに一致することもできます。
最後の構文は1つです。上記で示したように、/posts/ のようにスラッシュで終わるパターンは、その文字列で始まるすべてのパスに一致します。末尾のスラッシュがあるパスのみに一致させるには、/posts/{$} と記述します。これは /posts/ に一致しますが、/posts や /posts/234 には一致しません。
そして、最後の API が1つあります。net/http.Request には SetPathValue メソッドがあり、標準ライブラリ以外のルーターが独自のパス解析の結果を Request.PathValue 経由で利用できるようにします。
優先順位
すべての HTTP ルーターは、/posts/{id} と /posts/latest のように、重複するパターンを処理する必要があります。これらのパターンは両方ともパス「posts/latest」に一致しますが、リクエストを処理できるのはせいぜい1つです。どのパターンが優先されますか?
一部のルーターは重複を許可しません。他のルーターは最後に登録されたパターンを使用します。Go は常に重複を許可し、登録順序に関係なく長いパターンを選択してきました。順序独立性を維持することは私たちにとって重要であり (そして後方互換性のために必要でした)、しかし「最長が勝つ」というルールよりも優れたルールが必要でした。そのルールでは、/posts/latest が /posts/{id} よりも選択されますが、両方よりも /posts/{identifier} が選択されます。これは間違っているように思えます。ワイルドカード名は関係ないはずです。/posts/latest は、多くのパスではなく単一のパスに一致するため、この競争には常に勝つべきだと感じます。
優れた優先順位ルールの探求は、パターンの多くの特性を考慮することにつながりました。たとえば、最も長いリテラル (ワイルドカードではない) プレフィックスを持つパターンを優先することを検討しました。それにより、/posts/latest が /posts/{id} よりも選択されます。しかし、/users/{u}/posts/latest と /users/{u}/posts/{id} を区別することはできず、前者が優先されるべきだと感じます。
最終的に、パターンの見た目ではなく、その意味に基づいてルールを選択しました。すべての有効なパターンは、一連のリクエストに一致します。たとえば、/posts/latest はパス /posts/latest を持つリクエストに一致しますが、/posts/{id} は最初のセグメントが「posts」である任意の2セグメントパスを持つリクエストに一致します。あるパターンが別のパターンよりも厳密なサブセットのリクエストに一致する場合、そのパターンはより具体的であると言います。パターン /posts/latest は /posts/{id} よりも具体的です。なぜなら、後者は前者が一致するすべてのリクエストに一致し、さらに多くのリクエストに一致するからです。
優先順位のルールは単純です。最も具体的なパターンが勝ちます。このルールは、posts/latest が posts/{id} よりも優先されるべきであり、/users/{u}/posts/latest が /users/{u}/posts/{id} よりも優先されるべきであるという私たちの直感に合致します。メソッドについても意味があります。たとえば、GET /posts/{id} は /posts/{id} よりも優先されます。なぜなら、前者は GET および HEAD リクエストのみに一致し、後者は任意のメソッドを持つリクエストに一致するからです。
「最も具体的なものが勝つ」というルールは、ワイルドカードや {$} のない元のパターンのパス部分に対する元の「最長が勝つ」ルールを一般化したものです。そのようなパターンは、一方が他方のプレフィックスである場合にのみ重複し、長い方がより具体的です。
2つのパターンが重複しているが、どちらもより具体的ではない場合はどうなりますか?たとえば、/posts/{id} と /{resource}/latest は両方とも /posts/latest に一致します。どちらが優先されるかについて明白な答えはないため、これらのパターンは互いに衝突すると見なします。両方を登録すると (どちらの順序でも!)、パニックになります。
優先順位のルールは、メソッドとパスに対しては上記とまったく同じように機能しますが、互換性を維持するためにホストについては1つの例外を設けました。2つのパターンがそうでなければ衝突し、一方がホストを持ち、もう一方が持たない場合、ホストを持つパターンが優先されます。
コンピュータサイエンスの学生は、正規表現と正規言語の美しい理論を思い出すかもしれません。各正規表現は正規言語、つまりその表現に一致する文字列のセットを選び出します。表現について話すよりも言語について話すことで、一部の質問はより簡単に提起および回答できます。私たちの優先順位ルールはこの理論に触発されました。実際、各ルーティングパターンは正規表現に対応し、一致するリクエストのセットは正規言語の役割を果たします。
表現ではなく言語によって優先順位を定義することで、記述と理解が容易になります。しかし、無限の可能性のあるセットに基づくルールには欠点があります。つまり、効率的に実装する方法が明確ではありません。セグメントごとにウォークすることで、2つのパターンが衝突するかどうかを判断できることが判明しました。大まかに言えば、一方のパターンがもう一方のパターンがワイルドカードを持つ場所にリテラルセグメントを持つ場合、それはより具体的です。しかし、両方向でリテラルがワイルドカードと一致する場合、パターンは衝突します。
ServeMux に新しいパターンが登録されると、以前に登録されたパターンとの衝突がチェックされます。しかし、すべてのパターンのペアをチェックすると、二次時間が必要になります。新しいパターンと衝突しないパターンをスキップするためにインデックスを使用します。実際には、それは非常にうまく機能します。いずれにしても、このチェックはパターンが登録されるとき、通常はサーバーの起動時に行われます。Go 1.22 での受信リクエストのマッチング時間は、以前のバージョンから大きく変化していません。
互換性
私たちは、新しい機能が古いバージョンの Go と互換性を持つようにあらゆる努力をしました。新しいパターン構文は古い構文のスーパーセットであり、新しい優先順位ルールは古いルールを一般化したものです。しかし、いくつかのエッジケースがあります。たとえば、以前のバージョンの Go では、ブレースを持つパターンを受け入れ、それらをリテラルとして扱いましたが、Go 1.22 ではブレースをワイルドカードに使用します。GODEBUG 設定 httpmuxgo121 は古い動作を復元します。
これらのルーティング強化の詳細については、net/http.ServeMux のドキュメントを参照してください。
次の記事: スライス上の堅牢な汎用関数
前の記事: Go 1.22 がリリースされました!
ブログインデックス