[インデックス 17838] ファイルの概要
このコミットは、Go言語の標準ライブラリ strings
パッケージ内の Replacer
、特に singleStringReplacer
におけるバグ修正に関するものです。具体的には、置換対象の文字列が入力文字列の先頭にマッチし、かつ、そのマッチによってまだ出力バッファに何も書き込まれていない場合に、Replacer
が正しく動作しないという問題が修正されました。このバグは、buf == nil
という条件が、実際には何もマッチしなかった場合ではなく、出力バッファが空であることのチェックとして誤用されていたことに起因します。
コミット
- コミットハッシュ:
2d6a13997a9e9b154b7761d41cdbc830e02fc18e
- Author: Brad Fitzpatrick bradfitz@golang.org
- Date: Thu Oct 24 15:51:19 2013 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2d6a13997a9e9b154b7761d41cdbc830e02fc18e
元コミット内容
strings: fix Replacer bug with prefix matches
singleStringReplacer had a bug where if a string was replaced
at the beginning and no output had yet been produced into the
temp buffer before matching ended, an invalid nil check (used
as a proxy for having matched anything) meant it always
returned its input.
Fixes #6659
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/16880043
変更の背景
この変更は、Go言語の strings
パッケージが提供する Replacer
型、特に単一の文字列置換を扱う singleStringReplacer
における特定のバグを修正するために行われました。
バグの具体的なシナリオは以下の通りです。
Replacer
が、入力文字列の先頭にマッチするパターンを検出する。- このマッチが発生する前に、出力バッファ(
buf
)にはまだ何も書き込まれていない。 Replacer
の内部ロジックが、マッチが全く発生しなかったかどうかを判断するためにbuf == nil
という条件を使用していた。
この条件は、本来「何も置換が行われなかった(つまり、入力文字列がそのまま返されるべき)」という状況を検出するために使われるべきでした。しかし、上記のシナリオでは、マッチは発生しているにもかかわらず、出力バッファがまだ空であるため buf == nil
が true
と評価されてしまい、結果として Replacer
は入力文字列をそのまま返してしまっていました。これは、置換が実際には行われたにもかかわらず、その結果が反映されないという誤った動作を引き起こしていました。
この問題は、GoのIssue 6659として報告されており、このコミットはその問題を解決することを目的としています。
前提知識の解説
Go言語の strings
パッケージ
strings
パッケージは、Go言語の標準ライブラリの一部であり、文字列操作のためのユーティリティ関数を提供します。文字列の検索、置換、分割、結合など、多岐にわたる機能が含まれています。
strings.Replacer
strings.Replacer
は、複数の置換ルールを効率的に適用するための型です。NewReplacer
関数を使って作成され、Replace
メソッドを呼び出すことで文字列の置換を実行します。例えば、strings.NewReplacer("old1", "new1", "old2", "new2")
のように、old
と new
のペアを複数指定できます。
singleStringReplacer
strings.Replacer
は、内部的に置換ルールの数や複雑さに応じて異なる実装を選択します。このコミットで言及されている singleStringReplacer
は、Replacer
が単一の置換ルール(例: strings.NewReplacer("Hello", "Hi")
)を持つ場合に最適化された内部実装です。この最適化は、単一のパターン検索に特化することで、より高速な置換を可能にします。
文字列置換の一般的なロジック
一般的な文字列置換のアルゴリズムは、入力文字列を走査し、置換パターンにマッチする部分を見つけ、それを置換後の文字列に置き換えていくというものです。この際、部分的に処理された文字列を一時的なバッファに蓄積していくのが一般的です。
finder.next(s[i:])
: これは、singleStringReplacer
が内部的に使用するパターン検索メカニズムの一部です。s[i:]
は、入力文字列の現在の位置i
から始まる部分文字列を示し、next
メソッドは、その部分文字列内でパターンが次にマッチする位置を返します。マッチが見つからない場合は-1
を返します。buf
(バイトスライス): Goでは文字列はイミュータブル(不変)であるため、文字列の変更は新しい文字列を作成することを意味します。Replacer
は、置換処理中に新しい文字列を効率的に構築するために、バイトスライス[]byte
を一時的なバッファとして使用します。このバッファに、置換されていない部分と置換された部分を順次追加していきます。
nil
と空のスライス
Goにおいて、nil
スライスと空のスライス([]byte{}
)は異なります。
nil
スライスは、基底配列を持たず、長さも容量も0です。- 空のスライスは、基底配列を持つことができ、長さも容量も0です。
この違いは、特にスライスが初期化されたかどうか、または何らかのデータが追加されたかどうかを判断する際に重要になります。このバグでは、buf
が初期化された直後(まだ何も追加されていない状態)では nil
であり、最初のマッチが入力文字列の先頭で発生した場合、buf
に何も追加されないまま次のループに進むため、buf == nil
が true
のままになってしまうことが問題でした。
技術的詳細
このバグは、singleStringReplacer
の Replace
メソッド内のロジックに潜んでいました。
元のコードでは、置換処理の最後に以下の条件分岐がありました。
if buf == nil {
return s
}
この if buf == nil
のチェックは、「置換が一度も行われなかった場合」、つまり入力文字列 s
がそのまま返されるべきかどうかを判断するために使用されていました。
しかし、このチェックには問題がありました。
buf
はvar buf []byte
として宣言されており、初期状態ではnil
です。- ループ内で
r.finder.next(s[i:])
が呼び出され、パターンがマッチした場合、match
変数にはマッチした位置が格納されます。 - もし
match
が0
(つまり、入力文字列の先頭でマッチ) であった場合、buf = append(buf, s[i:i+match]...)
の部分でs[i:i+0]
、つまり空のスライスがbuf
に追加されます。 append
関数は、nil
スライスに要素を追加した場合でも、結果としてnil
ではない(空の)スライスを返すことがあります。しかし、この特定のケース(append(nil, []byte{}...)
)では、Goのバージョンや実装によってはbuf
がnil
のままであるか、またはnil
ではないが空のスライスになる可能性がありました。- さらに重要なのは、
buf = append(buf, r.value...)
の部分で置換後の文字列がbuf
に追加されますが、もしr.value
が空文字列(例:NewReplacer("Hello", "")
のように、文字列を削除する置換)であった場合、buf
は依然としてnil
のままか、または空のままになる可能性がありました。
結果として、入力文字列の先頭でマッチが発生し、かつ置換後の文字列が空であるか、または最初のマッチが先頭で発生したために buf
にまだ実質的なデータが追加されていない状況では、buf == nil
が true
と評価されてしまい、Replace
メソッドは誤って元の入力文字列 s
をそのまま返してしまっていました。これは、置換が実際には行われたにもかかわらず、その結果が反映されないというバグでした。
このバグは、strings.NewReplacer("Hello", "")
のようなケースで、入力が "Hello"
や "Hellox"
の場合に顕著に現れました。本来であれば "Hello"
は ""
に、"Hellox"
は "x"
になるべきですが、バグのある実装ではそれぞれ "Hello"
と "Hellox"
が返されていました。
コアとなるコードの変更箇所
src/pkg/strings/replace.go
の singleStringReplacer.Replace
メソッドが変更されました。
--- a/src/pkg/strings/replace.go
+++ b/src/pkg/strings/replace.go
@@ -364,17 +364,18 @@ func makeSingleStringReplacer(pattern string, value string) *singleStringReplace
func (r *singleStringReplacer) Replace(s string) string {
var buf []byte
- i := 0
+ i, matched := 0, false
for {
match := r.finder.next(s[i:])
if match == -1 {
break
}
+ matched = true
buf = append(buf, s[i:i+match]...)
buf = append(buf, r.value...)\n i += match + len(r.finder.pattern)
}
- if buf == nil {\n+ if !matched {\n return s
}
buf = append(buf, s[i:]...)
また、src/pkg/strings/replace_test.go
に新しいテストケースが追加されました。
--- a/src/pkg/strings/replace_test.go
+++ b/src/pkg/strings/replace_test.go
@@ -261,10 +261,21 @@ func TestReplacer(t *testing.T) {\n \ttestCases = append(testCases,\n \t\ttestCase{abcMatcher, \"\", \"\"},\n \t\ttestCase{abcMatcher, \"ab\", \"ab\"},\n+\t\ttestCase{abcMatcher, \"abc\", \"[match]\"},\n \t\ttestCase{abcMatcher, \"abcd\", \"[match]d\"},\n \t\ttestCase{abcMatcher, \"cabcabcdabca\", \"c[match][match]d[match]a\"},\n \t)\n \n+\t// Issue 6659 cases (more single string replacer)\n+\n+\tnoHello := NewReplacer(\"Hello\", \"\")\n+\ttestCases = append(testCases,\n+\t\ttestCase{noHello, \"Hello\", \"\"},\n+\t\ttestCase{noHello, \"Hellox\", \"x\"},\n+\t\ttestCase{noHello, \"xHello\", \"x\"},\n+\t\ttestCase{noHello, \"xHellox\", \"xx\"},\n+\t)\n+\n \t// No-arg test cases.\n \n \tnop := NewReplacer()\n```
## コアとなるコードの解説
### `src/pkg/strings/replace.go` の変更
`singleStringReplacer.Replace` メソッドの変更は非常にシンプルですが、効果的です。
1. **`i, matched := 0, false`**:
- 元のコードでは `i := 0` のみでした。
- 変更後、`i` に加えて `matched` という新しいブール型変数が導入され、`false` で初期化されます。
- この `matched` 変数の目的は、**「少なくとも一度でもパターンマッチが成功したかどうか」**を正確に追跡することです。
2. **`matched = true` の追加**:
- `for` ループ内で `match := r.finder.next(s[i:])` の結果、`match == -1` でない(つまり、パターンが見つかった)場合に、`matched = true` が設定されます。
- これにより、たとえマッチが入力文字列の先頭で発生し、`buf` にまだ実質的なデータが追加されていなくても、マッチが成功したという事実が `matched` 変数によって記録されるようになります。
3. **`if buf == nil` から `if !matched` への変更**:
- これがバグ修正の核心です。
- 元の `if buf == nil` は、`buf` が `nil` であること(つまり、まだ何も書き込まれていない状態)を「何もマッチしなかった」ことの代理として使用していました。しかし、前述の通り、これは先頭マッチや空文字列への置換の場合に誤動作を引き起こしました。
- 変更後、`if !matched` とすることで、**「一度もパターンマッチが成功しなかった場合」**にのみ元の入力文字列 `s` を返すようにロジックが修正されました。これにより、`buf` の状態に依存せず、実際の置換の有無に基づいて正確な判断ができるようになりました。
この変更により、`singleStringReplacer` は、入力文字列の先頭でパターンがマッチし、かつ置換後の文字列が空である場合でも、正しく置換を実行し、期待される結果を返すようになります。
### `src/pkg/strings/replace_test.go` の変更
新しいテストケースが `TestReplacer` 関数に追加されました。これらは特にIssue 6659で報告されたシナリオをカバーしています。
```go
// Issue 6659 cases (more single string replacer)
noHello := NewReplacer("Hello", "")
testCases = append(testCases,
testCase{noHello, "Hello", ""},
testCase{noHello, "Hellox", "x"},
testCase{noHello, "xHello", "x"},
testCase{noHello, "xHellox", "xx"},
)
noHello := NewReplacer("Hello", "")
は、「Hello」という文字列を空文字列に置換するReplacer
を作成します。testCase{noHello, "Hello", ""}
: 入力文字列が置換対象の文字列そのもので、かつ先頭にマッチする場合のテスト。期待される出力は空文字列です。testCase{noHello, "Hellox", "x"}
: 入力文字列の先頭に置換対象の文字列がマッチし、その後に続く文字列がある場合のテスト。期待される出力は「x」です。testCase{noHello, "xHello", "x"}
: 置換対象の文字列が入力文字列の途中にあり、その前に文字列がある場合のテスト。期待される出力は「x」です。testCase{noHello, "xHellox", "xx"}
: 置換対象の文字列が入力文字列の途中にあり、その前後にも文字列がある場合のテスト。期待される出力は「xx」です。
これらのテストケースは、特に「先頭マッチ」と「空文字列への置換」という、元のバグが顕在化した状況を網羅しており、修正が正しく機能していることを検証します。
関連リンク
- Go CL (Code Review) リンク:
https://golang.org/cl/16880043
参考にした情報源リンク
- コミットデータ:
/home/orange/Project/comemo/commit_data/17838.txt
- GitHubコミットページ:
https://github.com/golang/go/commit/2d6a13997a9e9b154b7761d41cdbc830e02fc18e
- Go Issue 6659 (直接的な検索結果は見つかりませんでしたが、コミットメッセージに記載されています)
- Go言語の
strings
パッケージのドキュメント (一般的な知識として) - Go言語のスライスに関するドキュメント (nilスライスと空スライスの違いに関する一般的な知識として)