Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14901] ファイルの概要

このコミットは、Go言語のドキュメンテーションツールである godoc コマンドに関連するものです。godoc は、Goのソースコードからドキュメントを生成し、HTTPサーバーとして提供するツールです。src/cmd/godoc/godoc.go ファイルは、この godoc サーバーの主要なロジック、特にHTTPリクエストのハンドリングとリダイレクト処理を実装しています。

コミット

cmd/godoc におけるリダイレクト処理の修正。リダイレクト時にクエリ文字列が失われないように変更され、http://golang.org/pkg/runtime?m=all のようなURLが正しく機能するようになりました。

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c2aee3c0bf7b4199d71970df5557b28b53afae4f

元コミット内容

cmd/godoc: when redirecting don't clear query string
so that http://golang.org/pkg/runtime?m=all works.

R=bradfitz
CC=golang-dev
https://golang.org/cl/7094046

変更の背景

この変更の背景には、godoc サーバーが特定のURLパターンに対してリダイレクトを行う際に、HTTPリクエストのクエリ文字列(URLの ? 以降の部分)が意図せず失われてしまうという問題がありました。

具体的には、http://golang.org/pkg/runtime?m=all のようなURLは、runtime パッケージのドキュメントを表示しつつ、m=all というクエリパラメータによって、通常は非表示になっているメソッドやフィールドもすべて表示するという機能を提供していました。しかし、godoc サーバーが内部的にURLの正規化やパスの修正のためにリダイレクトを行う際、既存の実装では http.Redirect 関数に直接新しいパスを渡していました。この http.Redirect の挙動は、新しいURLが絶対パスで指定された場合、元のリクエストのクエリ文字列を破棄してしまうことがあります。

結果として、ユーザーが ?m=all のようなクエリパラメータを含むURLにアクセスしても、リダイレクト後にそのパラメータが失われ、期待する表示(例: 全てのメソッド表示)が得られないという不具合が発生していました。このコミットは、この問題を解決し、クエリ文字列がリダイレクト後も保持されるようにするために行われました。

前提知識の解説

  • godoc コマンド: Go言語の標準ツールの一つで、Goのソースコードからドキュメントを生成し、Webブラウザで閲覧できるようにHTTPサーバーとして提供します。Goのパッケージや型のドキュメント、コード例などを簡単に参照できるため、Go開発者にとって非常に重要なツールです。
  • HTTPリダイレクト (HTTP Redirect): Webサーバーがクライアント(ブラウザなど)に対して、要求されたリソースが別のURLに移動したことを伝える仕組みです。HTTPステータスコード(例: 301 Moved Permanently, 302 Found)と Location ヘッダーを使って行われます。クライアントは Location ヘッダーに示された新しいURLに再度リクエストを送信します。
  • URLの構成要素:
    • スキーム (Scheme): http://https:// の部分。
    • ホスト (Host): golang.org の部分。
    • パス (Path): /pkg/runtime の部分。リソースの場所を示します。
    • クエリ文字列 (Query String): ?m=all の部分。キーと値のペア(例: m=all)で構成され、サーバーに渡す追加のパラメータを指定します。
    • フラグメント (Fragment): #section のように # 以降の部分。ブラウザ内で特定のセクションにジャンプするために使われ、サーバーには送信されません。
  • net/http パッケージ: Go言語の標準ライブラリで、HTTPクライアントとサーバーの実装を提供します。
    • http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェース。
    • *http.Request: 受信したHTTPリクエストを表す構造体。リクエストメソッド、URL、ヘッダー、ボディなどの情報を含みます。
    • http.Redirect(w http.ResponseWriter, r *http.Request, url string, code int): 指定されたURLにクライアントをリダイレクトするためのヘルパー関数。url 引数に新しいリダイレクト先のURLを指定します。
    • *url.URL 構造体: net/url パッケージで定義されており、URLの各構成要素(スキーム、ホスト、パス、クエリなど)をフィールドとして持つ構造体です。URLの解析や構築に便利です。

技術的詳細

このコミットの技術的な核心は、Goの net/http パッケージにおける http.Redirect 関数の挙動と、net/url.URL 構造体の利用方法にあります。

元のコードでは、http.Redirect(w, r, canonical, http.StatusMovedPermanently) のように、リダイレクト先のパス(canonical)を直接文字列として http.Redirect 関数に渡していました。http.Redirect 関数は内部的に url.Parse を使用してURLを解析し、Location ヘッダーを設定します。しかし、url 引数に絶対パス(例: /pkg/runtime/)のみが与えられた場合、元のリクエスト (r) のクエリ文字列は新しいリダイレクトURLには引き継がれませんでした。これは、http.Redirecturl 引数を新しい完全なURLとして解釈し、元のリクエストのURLからパス以外の情報を自動的にマージしないためです。

この問題を解決するために、変更後のコードでは以下の手順を踏んでいます。

  1. r.URL のコピー: url := *r.URL という行で、現在のリクエスト (r) のURL (r.URL) のコピーを作成しています。r.URL*url.URL 型であり、これには元のリクエストのスキーム、ホスト、パス、クエリ文字列など、すべてのURL構成要素が含まれています。ポインタのデリファレンス *r.URL を使うことで、元の url.URL 構造体の値が新しい変数 url にコピーされます。これにより、元のリクエストのクエリ文字列が url 変数に引き継がれます。
  2. パスの更新: url.Path = canonical という行で、コピーした url 構造体の Path フィールドのみを、正規化された新しいパス (canonical) に更新します。この際、url 構造体の他のフィールド(特に RawQuery フィールドに格納されているクエリ文字列)は変更されません。
  3. 新しいURL文字列の生成: url.String() メソッドを呼び出すことで、更新された url 構造体から完全なURL文字列を生成します。この String() メソッドは、url.URL 構造体のすべての構成要素(スキーム、ホスト、パス、クエリ文字列など)を考慮して、正しい形式のURL文字列を構築します。
  4. リダイレクトの実行: 最後に、http.Redirect(w, r, url.String(), http.StatusMovedPermanently) を呼び出し、url.String() で生成された完全なURLをリダイレクト先として指定します。これにより、クエリ文字列が保持された状態でリダイレクトが行われます。

このアプローチにより、godoc はURLのパスを正規化しつつも、ユーザーが指定したクエリパラメータを失うことなく、期待通りの動作を維持できるようになりました。

コアとなるコードの変更箇所

--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -656,7 +656,9 @@ func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
 		canonical += "/"
 	}
 	if r.URL.Path != canonical {
-		http.Redirect(w, r, canonical, http.StatusMovedPermanently)
+		url := *r.URL
+		url.Path = canonical
+		http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
 		redirected = true
 	}
 	return
@@ -668,7 +670,9 @@ func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
 		c = c[:len(c)-1]
 	}
 	if r.URL.Path != c {
-		http.Redirect(w, r, c, http.StatusMovedPermanently)
+		url := *r.URL
+		url.Path = c
+		http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
 		redirected = true
 	}
 	return

コアとなるコードの解説

このコミットでは、src/cmd/godoc/godoc.go ファイル内の redirect 関数と redirectFile 関数の2箇所が変更されています。両方の関数で同様の修正が適用されています。

redirect 関数の変更点

 // 変更前
 if r.URL.Path != canonical {
-	http.Redirect(w, r, canonical, http.StatusMovedPermanently)
+	url := *r.URL
+	url.Path = canonical
+	http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
 	redirected = true
 }
  • url := *r.URL:
    • r.URL は現在のHTTPリクエストのURLを表す *url.URL 型のポインタです。
    • *r.URL はそのポインタが指す url.URL 構造体の値そのものを取得します。
    • この行は、現在のリクエストのURL(スキーム、ホスト、パス、クエリ文字列など全てを含む)のコピーを新しい変数 url に作成しています。これにより、元のリクエストのクエリ文字列が url 変数に引き継がれます。
  • url.Path = canonical:
    • コピーした url 構造体の Path フィールドを、正規化された新しいパス canonical に更新します。
    • この操作は url 構造体の Path フィールドのみを変更し、RawQuery フィールド(クエリ文字列を保持)を含む他のフィールドには影響を与えません。
  • http.Redirect(w, r, url.String(), http.StatusMovedPermanently):
    • url.String() メソッドは、更新された url 構造体から完全なURL文字列を生成します。この文字列には、元のリクエストから引き継がれたクエリ文字列と、新しく設定されたパスが含まれます。
    • この完全なURL文字列を http.Redirect 関数に渡すことで、リダイレクト時にクエリ文字列が保持されるようになります。

redirectFile 関数の変更点

redirectFile 関数も同様の目的(ファイルパスのリダイレクト)を持つため、全く同じロジックで修正されています。

 // 変更前
 if r.URL.Path != c {
-	http.Redirect(w, r, c, http.StatusMovedPermanently)
+	url := *r.URL
+	url.Path = c
+	http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
 	redirected = true
 }

ここでも、c は正規化されたファイルパスを表す文字列であり、redirect 関数における canonical と同様に扱われます。変更の意図と効果は redirect 関数と同じです。

これらの変更により、godoc サーバーはURLのパスを修正するリダイレクトを行う際にも、元のリクエストに含まれるクエリ文字列を正しく保持し、ユーザーエクスペリエンスを向上させました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • net/http および net/url パッケージのソースコード
  • HTTPリダイレクトに関する一般的なWeb技術情報