Goブログ
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つの新しいメソッドだけです。
各投稿に整数IDを持つ架空のブログサーバーを使用して、変更点を説明します。GET /posts/234
のようなリクエストは、ID 234の投稿を取得します。Go 1.22以前では、これらのリクエストを処理するコードは、次のような行で始まります
http.HandleFunc("/posts/", handlePost)
末尾のスラッシュは、/posts/
で始まるすべてのリクエストをhandlePost
関数にルーティングします。この関数は、HTTPメソッドがGETであることを確認し、IDを抽出し、投稿を取得する必要があります。メソッドチェックはリクエストを満たすために厳密には必要ないため、省略するのは自然な間違いです。つまり、DELETE /posts/234
のようなリクエストでも投稿がフェッチされ、少なくとも意外な結果になります。
Go 1.22では、既存のコードは引き続き機能しますが、代わりに次のように記述することもできます。
http.HandleFunc("GET /posts/{id}", handlePost2)
このパターンは、パスが「/posts/」で始まり、2つのセグメントを持つGETリクエストに一致します。(特別なケースとして、GETはHEADにも一致します。他のすべてのメソッドは正確に一致します。)handlePost2
関数はメソッドをチェックする必要がなくなり、ID文字列の抽出は、Request
の新しいPathValue
メソッドを使用して記述できます。
idString := req.PathValue("id")
handlePost2
の残りの部分はhandlePost
のように動作し、文字列IDを整数に変換して投稿を取得します。
他の一致するパターンが登録されていない場合、DELETE /posts/234
のようなリクエストは失敗します。HTTPセマンティクスに従って、net/http
サーバーは、そのようなリクエストに対して、利用可能なメソッドをAllow
ヘッダーにリストした405 Method Not Allowed
エラーを返します。
ワイルドカードは、上記の例のように{id}
のようなセグメント全体に一致するか、...
で終わる場合は、パターン/files/{pathname...}
のように、パスの残りのセグメントすべてに一致できます。
最後に1つの構文があります。上記で示したように、/posts/
のようなスラッシュで終わるパターンは、その文字列で始まるすべてのパスに一致します。末尾のスラッシュを持つパスのみを一致させるには、/posts/{$}
と記述できます。これは/posts/
に一致しますが、/posts
や/posts/234
には一致しません。
そして、最後のAPIがあります。net/http.Request
にはSetPathValue
メソッドがあり、標準ライブラリ外のルーターは、独自のパスパースの結果をRequest.PathValue
を介して利用可能にできます。
優先順位
すべてのHTTPルーターは、/posts/{id}
と/posts/latest
のような重複するパターンを処理する必要があります。これらの両方のパターンはパス「posts/latest」に一致しますが、リクエストを処理できるのは最大で1つだけです。どのパターンが優先されますか?
一部のルーターは重複を許可しません。他のルーターは、最後に登録されたパターンを使用します。Goは常に重複を許可しており、登録順に関係なく、より長いパターンを選択してきました。順序の独立性を維持することは私たちにとって重要でした(そして下位互換性のために必要でした)が、「最長が優先」よりも優れたルールが必要でした。そのルールは/posts/{id}
よりも/posts/latest
を選択しますが、両方よりも/posts/{identifier}
を選択します。これは間違っているように見えます。ワイルドカードの名前は重要ではありません。/posts/latest
は常にこの競合で優先されるべきです。なぜなら、多くのパスではなく、単一のパスに一致するからです。
優れた優先順位ルールを探す中で、パターンの多くの特性を検討しました。たとえば、最長のリテラル(ワイルドカードではない)プレフィックスを持つパターンを優先することを検討しました。これにより、/posts/{id}
よりも/posts/latest
が選択されます。しかし、/users/{u}/posts/latest
と/users/{u}/posts/{id}
を区別せず、前者が優先されるように見えます。
最終的に、パターンの見た目ではなく、パターンの意味に基づいたルールを選択しました。有効なパターンはすべて、リクエストのセットに一致します。たとえば、/posts/latest
はパス/posts/latest
のリクエストに一致しますが、/posts/{id}
は最初のセグメントが「posts」である2セグメントパスのリクエストに一致します。あるパターンが別のものよりもより具体的であるとは、リクエストの厳密なサブセットに一致する場合を言います。後者は前者に一致するすべてのリクエストとさらに多くのリクエストに一致するため、パターン/posts/latest
は/posts/{id}
よりも具体的です。
優先順位ルールは簡単です。最も具体的なパターンが優先されます。このルールは、posts/latests
がposts/{id}
よりも優先され、/users/{u}/posts/latest
が/users/{u}/posts/{id}
よりも優先されるという私たちの直感と一致します。メソッドに対しても理にかなっています。たとえば、GET /posts/{id}
は/posts/{id}
よりも優先されます。なぜなら、前者はGETとHEADのリクエストのみに一致し、後者は任意のメソッドのリクエストに一致するからです。
「最も具体的なものが優先」というルールは、ワイルドカードや{$}
のない元の、パスの部分に対する元の「最長が優先」ルールを一般化します。そのようなパターンは、一方が他方のプレフィックスである場合にのみ重複し、より長い方がより具体的です。
2つのパターンが重複するが、どちらもより具体的でない場合はどうなりますか?たとえば、/posts/{id}
と/{resource}/latest
はどちらも/posts/latest
に一致します。どちらが優先されるかには明確な答えがないため、これら2つのパターンはお互いに競合すると見なされます。これら両方を(どちらの順序でも!)登録しようとすると、パニックになります。
優先順位ルールはメソッドとパスに対して上記のように正確に機能しますが、互換性を維持するためにホストに対して1つの例外を作成する必要がありました。2つのパターンがそうでなければ競合し、一方がホストを持ち、もう一方が持たない場合、ホストを持つパターンが優先されます。
コンピュータサイエンスの学生は、正規表現と正規言語の美しい理論を思い出すかもしれません。各正規表現は正規言語、つまり表現によって一致する文字列の集合を選び出します。表現ではなく言語について話すことで、いくつかの質問はより簡単に提起して回答できます。私たちの優先順位ルールはこの理論に触発されました。実際、各ルーティングパターンは正規表現に対応し、一致するリクエストのセットは正規言語の役割を果たします。
言語ではなく表現によって優先順位を定義すると、簡単に記述して理解できます。しかし、潜在的に無限の集合に基づいたルールには欠点があります。効率的に実装する方法が明確ではありません。2つのパターンが競合するかどうかは、セグメントごとにそれらを辿ることで判断できます。大まかに言うと、あるパターンが別のパターンにワイルドカードがあるところにリテラルセグメントを持つ場合、それはより具体的です。しかし、両方向でリテラルがワイルドカードと一致する場合は、パターンが競合します。
新しいパターンがServeMux
に登録されると、以前に登録されたパターンとの競合をチェックします。しかし、すべてのペアのパターンをチェックすると、2乗の時間がかかります。新しいパターンと競合する可能性のないパターンをスキップするためにインデックスを使用します。実際には非常にうまく機能します。いずれにしても、このチェックは、通常はサーバー起動時に、パターンが登録されるときに発生します。Go 1.22での着信リクエストの一致時間は、以前のバージョンとほとんど変わっていません。
互換性
新しい機能を古いバージョンのGoと互換性のあるようにするためにあらゆる努力をしました。新しいパターン構文は古い構文のスーパーセットであり、新しい優先順位ルールは古いルールを一般化します。しかし、いくつかの極端なケースがあります。たとえば、以前のバージョンのGoは中括弧付きのパターンを受け入れ、文字通りに扱っていましたが、Go 1.22では中括弧をワイルドカードに使用します。GODEBUG設定httpmuxgo121
は古い動作を復元します。
これらのルーティング機能強化の詳細については、net/http.ServeMux
ドキュメントを参照してください。
次の記事: スライスに対する堅牢なジェネリック関数
前の記事: Go 1.22がリリースされました!
ブログインデックス