[インデックス 15044] ファイルの概要
このコミットは、Go言語のドキュメンテーションツールである godoc コマンドの src/cmd/godoc/godoc.go ファイルにおけるバグ修正です。godoc は、Goのソースコードからドキュメントを生成し、HTTPサーバーとして提供するツールです。このファイルは、godoc サーバーがHTTPリクエストを処理する際のURLリダイレクトロジックを実装しています。具体的には、URLの正規化(末尾のスラッシュの有無の調整)に関する処理が含まれています。
コミット
cmd/godoc: fix buggy use of strings.HasSuffix
This code never worked. Maybe it's not necessary?
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/7225070
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7e9f00c80c56fe325d0c0e11c4a8ce4bc368b06e
元コミット内容
cmd/godoc: fix buggy use of strings.HasSuffix
このコミットは、godoc コマンド内で strings.HasSuffix 関数の誤った使用方法を修正するものです。コミットメッセージには「このコードは一度も機能しなかった。もしかしたら不要なのかもしれない?」とあり、既存のコードが意図した動作をしていなかったことを示唆しています。
変更の背景
godoc サーバーは、リクエストされたURLのパスを正規化し、必要に応じてリダイレクトを行う機能を持っています。例えば、ディレクトリを示すURLには末尾にスラッシュを付与したり、ファイルを示すURLからは末尾のスラッシュを除去したりすることが一般的です。
このコミット以前のコードでは、このURL正規化のロジックにおいて strings.HasSuffix 関数が誤った引数で呼び出されていました。strings.HasSuffix(s, suffix) は文字列 s が suffix で終わるかを判定する関数ですが、元のコードでは strings.HasSuffix("/", canonical) のように、定数文字列である "/" が s の位置に、動的に変化するパスである canonical が suffix の位置に渡されていました。
これにより、"/" が canonical で終わるかという、ほとんど常に false となる条件が評価され、結果として末尾のスラッシュの追加や削除を行うべきロジックが全く機能していませんでした。コミットメッセージにある「このコードは一度も機能しなかった」という記述は、この引数の誤用によるものです。この修正は、godoc がURLの正規化を正しく行い、ユーザーエクスペリエンスを向上させるために不可欠でした。
前提知識の解説
1. strings.HasSuffix 関数
Go言語の標準ライブラリ strings パッケージに含まれる関数です。
func HasSuffix(s, suffix string) bool
この関数は、文字列 s が指定された suffix で終わる場合に true を返します。そうでない場合は false を返します。
例:
strings.HasSuffix("filename.go", ".go") は true を返します。
strings.HasSuffix("/path/to/dir/", "/") は true を返します。
strings.HasSuffix("/", "/path/to/dir/") は false を返します("/" が /path/to/dir/ で終わることはないため)。
2. pathpkg.Clean (実際には path.Clean または filepath.Clean)
Go言語の標準ライブラリ path または path/filepath パッケージに含まれる関数です。
func Clean(path string) string
この関数は、パスを「クリーン」な形式に変換します。具体的には、以下のような処理を行います。
/a/b/../cのような.や..を解決します。/a//bのような連続するスラッシュを単一のスラッシュに置き換えます。- 末尾のスラッシュを削除します(ルートパス
/を除く)。 - 空のパスは
.に変換します。 例:path.Clean("/a/b/../c")は/a/cを返します。path.Clean("/a//b/")は/a/bを返します。
godoc の文脈では、http.Request.URL.Path から取得したパスを正規化するために使用されます。
3. strings.TrimRight 関数
Go言語の標準ライブラリ strings パッケージに含まれる関数です。
func TrimRight(s, cutset string) string
この関数は、文字列 s の末尾から cutset に含まれる文字をすべて削除した新しい文字列を返します。
例:
strings.TrimRight("banana", "an") は "b" を返します(末尾から 'a', 'n', 'a', 'n' が削除される)。
strings.TrimRight("/path/to/dir///", "/") は /path/to/dir を返します。
4. URLの正規化とリダイレクト
Webサーバーでは、同じリソースに対して複数のURLでアクセスできると、SEO上の問題やキャッシュの非効率性につながることがあります。そのため、URLを統一された形式(正規化された形式)に変換し、元のURLから正規化されたURLへHTTPリダイレクト(通常は301 Moved Permanently)を行うことが一般的です。
godoc の場合、ディレクトリを示すパスには末尾にスラッシュを付ける(例: /pkg/fmt を /pkg/fmt/ に)、ファイルを示すパスからは末尾のスラッシュを削除する(例: /pkg/fmt/doc.go/ を /pkg/fmt/doc.go に)といったルールが適用されます。
5. http.ResponseWriter と http.Request
Go言語の net/http パッケージにおけるHTTPハンドラの基本的なインターフェースと構造体です。
http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェース。ステータスコードの設定、ヘッダーの追加、レスポンスボディの書き込みなどを行います。http.Request: 受信したHTTPリクエストに関する情報(URL、メソッド、ヘッダー、ボディなど)を保持する構造体。
技術的詳細
このコミットは、godoc の godoc.go ファイル内の2つの関数 redirect と redirectFile における strings.HasSuffix の誤用を修正しています。
redirect 関数における修正
redirect 関数は、主にディレクトリパスの正規化を担当しています。pathpkg.Clean はパスの末尾のスラッシュを削除する特性があるため、ディレクトリを示すパスの場合、pathpkg.Clean の結果に末尾のスラッシュを再付与する必要があります。
元のコード:
if !strings.HasSuffix("/", canonical) {
canonical += "/"
}
この if 文は、canonical パスが末尾にスラッシュを持たない場合にスラッシュを追加しようとしています。しかし、strings.HasSuffix("/", canonical) は、文字列 "/" が canonical で終わるかをチェックします。これは、canonical が正確に "/" である場合を除き、常に false を返します。例えば canonical が "/pkg/fmt" であっても、strings.HasSuffix("/", "/pkg/fmt") は false です。結果として、この条件は常に真となり、canonical が既にスラッシュで終わっているかどうかにかかわらず、常にスラッシュが追加されてしまうか、あるいは意図したチェックが全く行われない状態でした。
修正後のコード:
if !strings.HasSuffix(canonical, "/") {
canonical += "/"
}
修正後は、strings.HasSuffix(canonical, "/") となり、canonical パスが文字列 "/" で終わるかを正しくチェックします。これにより、canonical が末尾にスラッシュを持たない場合にのみスラッシュが追加されるようになり、意図したディレクトリパスの正規化が正しく機能するようになりました。
redirectFile 関数における修正
redirectFile 関数は、主にファイルパスの正規化を担当しており、末尾のスラッシュを削除することを目的としています。
元のコード:
for strings.HasSuffix("/", c) {
c = c[:len(c)-1]
}
この for ループは、パス c の末尾からスラッシュを繰り返し削除しようとしています。しかし、ここでも strings.HasSuffix("/", c) という誤った引数順序が使われています。前述の通り、この条件は c が正確に "/" である場合を除き、常に false を返します。したがって、このループはほとんどの場合実行されず、ファイルパスの末尾のスラッシュが削除されることはありませんでした。
修正後のコード:
c = strings.TrimRight(c, "/")
修正後は、strings.TrimRight(c, "/") を使用しています。この関数は、文字列 c の末尾から指定された cutset(この場合は "/")に含まれる文字をすべて削除します。これにより、パス c の末尾に存在するすべてのスラッシュが効率的かつ正確に削除されるようになり、ファイルパスの正規化が正しく機能するようになりました。
この修正により、godoc サーバーはURLの正規化を意図通りに行い、ユーザーがアクセスするURLが統一され、より予測可能な動作をするようになりました。
コアとなるコードの変更箇所
--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -652,7 +652,7 @@ func applyTemplate(t *template.Template, name string, data interface{}) []byte {
func redirect(w http.ResponseWriter, r *http.Request) (redirected bool) {
canonical := pathpkg.Clean(r.URL.Path)
- if !strings.HasSuffix("/", canonical) {
+ if !strings.HasSuffix(canonical, "/") {
canonical += "/"
}
if r.URL.Path != canonical {
@@ -666,9 +666,7 @@ func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
func redirectFile(w http.ResponseWriter, r *http.Request) (redirected bool) {
c := pathpkg.Clean(r.URL.Path)
- for strings.HasSuffix("/", c) {
- \tc = c[:len(c)-1]\
- }
+ c = strings.TrimRight(c, "/")
if r.URL.Path != c {
url := *r.URL
url.Path = c
コアとなるコードの解説
redirect 関数内の変更
-
変更前 (
-行):if !strings.HasSuffix("/", canonical) {この行は、
canonicalというパス文字列が末尾にスラッシュを持っているかどうかを判定しようとしていました。しかし、strings.HasSuffix(s, suffix)の引数が逆になっており、sには定数文字列"/"が、suffixには動的なパスcanonicalが渡されていました。これは「"/"という文字列がcanonicalで終わるか?」という、ほとんど常にfalseとなる無意味なチェックでした。結果として、このif文の条件は常に真となり、次の行で無条件にスラッシュが追加されるか、あるいは意図したチェックが全く行われない状態でした。 -
変更後 (
+行):if !strings.HasSuffix(canonical, "/") {この行では、引数の順序が修正されました。
sにはパス文字列canonicalが、suffixには定数文字列"/"が渡されています。これにより、「canonicalというパス文字列が"/"で終わらない場合」という正しい条件が評価されるようになりました。この修正により、ディレクトリを示すパスにのみ末尾のスラッシュが適切に追加されるようになります。
redirectFile 関数内の変更
-
変更前 (
-行):for strings.HasSuffix("/", c) { c = c[:len(c)-1] }このコードブロックは、パス文字列
cの末尾からスラッシュを削除しようとしていました。しかし、ここでもstrings.HasSuffix("/", c)という誤った引数順序が使われていました。この条件は、cが正確に"/"である場合を除き、常にfalseを返します。そのため、このforループの本体(c = c[:len(c)-1])はほとんど実行されず、ファイルパスの末尾のスラッシュが削除されることはありませんでした。 -
変更後 (
+行):c = strings.TrimRight(c, "/")この行では、
strings.TrimRight関数が導入されました。strings.TrimRight(c, "/")は、文字列cの末尾から、引数cutsetで指定された文字(この場合は"/")をすべて削除した新しい文字列を返します。これにより、forループと手動での文字列操作を行うことなく、パスの末尾にある不要なスラッシュを効率的かつ正確に削除できるようになりました。これは、コードの簡潔さと正確性を向上させる優れた変更です。
これらの変更により、godoc はURLの正規化を正しく行い、ユーザーがアクセスするURLが統一され、より予測可能な動作をするようになりました。
関連リンク
- Go言語
stringsパッケージ: https://pkg.go.dev/strings - Go言語
pathパッケージ: https://pkg.go.dev/path - Go言語
net/httpパッケージ: https://pkg.go.dev/net/http
参考にした情報源リンク
- 特になし(Go標準ライブラリのドキュメントとコミットの差分から直接分析しました)。