[インデックス 19224] ファイルの概要
このコミットは、Go言語の標準ライブラリstrings
パッケージ内のテストファイルstrings_test.go
におけるオフバイワンエラー(off-by-one error)を修正するものです。具体的には、equal
関数内の文字列比較ロジックにおいて、インデックスの境界チェックが不適切であったために発生するパニック(panic)を解消します。
コミット
このコミットは、strings
パッケージのテストコードにおけるバグ修正です。Split
関数によって分割された文字列スライスを比較する際に、一方の文字列がもう一方よりも長い場合に発生するインデックス範囲外アクセス(out-of-bound access)によるパニックを防ぐための変更が加えられています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7ff8e90eb7ceb2016aa9fc736febd8a5902ec65e
元コミット内容
commit 7ff8e90eb7ceb2016aa9fc736febd8a5902ec65e
Author: Rui Ueyama <ruiu@google.com>
Date: Mon Apr 21 17:00:27 2014 -0700
strings: fix off-by-one error in test
Previously it would panic because of out-of-bound access
if s1 is longer than s2.
LGTM=iant
R=golang-codereviews, iant
CC=golang-codereviews
https://golang.org/cl/90110043
変更の背景
この変更は、strings
パッケージのテストスイートにおいて、特定の条件下で発生するランタイムパニックを修正するために行われました。問題はequal
というヘルパー関数内で発生していました。この関数は、2つの文字列s1
とs2
を比較するために、まずそれぞれを空文字列""
でSplit
し、その結果得られるルーン(rune)のスライスe1
とe2
を要素ごとに比較していました。
元のコードでは、ループ内でe1
のインデックスi
がe2
の長さを超えた場合にループを抜けるif i > len(e2)
という条件がありました。しかし、Goのスライスは0からlen(slice)-1
までのインデックスを持ちます。したがって、i
がlen(e2)
と等しくなった時点で、e2[i]
にアクセスしようとするとインデックス範囲外エラーが発生し、パニックを引き起こす可能性がありました。特に、s1
がs2
よりも長い場合にこの問題が顕在化しました。この修正は、テストの堅牢性を高め、誤ったパニックを回避することを目的としています。
前提知識の解説
- Go言語の文字列とルーン (Rune): Go言語において、文字列は読み取り専用のバイトスライスとして扱われます。UTF-8エンコーディングが標準であり、文字列内の個々のUnicodeコードポイントは「ルーン(rune)」として表現されます。
strings.Split(s, "")
のように空文字列で分割すると、文字列は個々のルーンに対応する文字列のスライスに分割されます。 - スライス (Slice): Goのスライスは、配列のセグメントを参照するデータ構造です。スライスは長さ(
len
)と容量(cap
)を持ちます。スライス内の要素にアクセスする際は、0
からlen(slice)-1
までのインデックスを使用します。この範囲外のインデックスにアクセスしようとすると、ランタイムパニックが発生します。 - オフバイワンエラー (Off-by-one error): プログラミングにおける一般的なエラーの一種で、ループの境界条件や配列のインデックス計算において、期待される値よりも1つ多く、または1つ少なく処理してしまうことで発生します。例えば、
len(slice)
は要素の総数を示しますが、有効な最大インデックスはlen(slice)-1
です。 - パニック (Panic): Go言語におけるパニックは、プログラムの実行を停止させる回復不可能なエラー状態です。通常、インデックス範囲外アクセス、nilポインタ参照、ゼロ除算などのプログラミングエラーによって引き起こされます。テスト中にパニックが発生することは、テスト対象のコードまたはテストコード自体にバグがあることを示します。
strings.Split
関数:strings
パッケージのSplit
関数は、指定されたセパレータに基づいて文字列を部分文字列のスライスに分割します。セパレータが空文字列の場合、文字列は各UTF-8エンコードされたルーンの後に分割されます。
技術的詳細
このコミットの技術的詳細は、strings_test.go
ファイル内のequal
関数にあります。この関数は、2つの文字列s1
とs2
が等しいかどうかを、Split
関数を使ってルーン単位で比較することで検証しています。
元のコードの関連部分は以下の通りでした。
func equal(m string, s1, s2 string, t *testing.T) bool {
e1 := Split(s1, "")
e2 := Split(s2, "")
for i, c1 := range e1 {
if i > len(e2) { // 問題の行
break
}
// ... 後続の比較ロジック ...
}
// ...
}
ここで問題となるのはif i > len(e2)
という条件です。Goのスライスe2
の有効なインデックスは0
からlen(e2)-1
までです。for i, c1 := range e1
ループはe1
の要素をi
に0
から順に割り当てていきます。
もしs1
がs2
よりも長く、したがってlen(e1)
がlen(e2)
よりも大きい場合を考えます。
例えば、len(e2)
が5だとします。有効なインデックスは0, 1, 2, 3, 4です。
ループ変数i
が4のとき、i > len(e2)
(4 > 5
)はfalse
なので、ループは続行されます。
次にi
が5になったとき、i > len(e2)
(5 > 5
)はまだfalse
です。この時点で、e2[i]
、つまりe2[5]
にアクセスしようとすると、インデックス5はe2
の範囲外であるため、パニックが発生します。
修正後のコードは以下の通りです。
func equal(m string, s1, s2 string, t *testing.T) bool {
e1 := Split(s1, "")
e2 := Split(s2, "")
for i, c1 := range e1 {
if i >= len(e2) { // 修正された行
break
}
// ... 後続の比較ロジック ...
}
// ...
}
変更点はi > len(e2)
がi >= len(e2)
になったことです。
これにより、i
がlen(e2)
と等しくなった時点で、e2
の有効なインデックス範囲外にアクセスする前にループがbreak
されるようになります。例えば、len(e2)
が5の場合、i
が5になった時点でi >= len(e2)
(5 >= 5
)はtrue
となり、ループが終了します。これにより、e2[5]
への不正なアクセスが回避され、パニックが防止されます。
この修正は、テストコードの正確性を保証し、テストが本来検出したいバグ以外の原因で失敗しないようにするために重要です。
コアとなるコードの変更箇所
変更はsrc/pkg/strings/strings_test.go
ファイルの一箇所のみです。
--- a/src/pkg/strings/strings_test.go
+++ b/src/pkg/strings/strings_test.go
@@ -652,7 +652,7 @@ func equal(m string, s1, s2 string, t *testing.T) bool {
e1 := Split(s1, "")
e2 := Split(s2, "")
for i, c1 := range e1 {
- if i > len(e2) {
+ if i >= len(e2) {
break
}
r1, _ := utf8.DecodeRuneInString(c1)
コアとなるコードの解説
変更された行はif i > len(e2) {
からif i >= len(e2) {
への修正です。
- 変更前 (
if i > len(e2)
): この条件では、ループ変数i
がe2
の長さ(len(e2)
)を「厳密に超えた」場合にのみループを中断します。しかし、Goのスライスのインデックスは0
からlen(slice)-1
までです。したがって、i
がlen(e2)
と等しい場合(つまり、e2
の最後の有効なインデックスlen(e2)-1
の次のインデックス)には、この条件はまだfalse
となり、ループは続行されます。その結果、e2[i]
にアクセスしようとすると、インデックスが範囲外となりパニックが発生します。 - 変更後 (
if i >= len(e2)
): この条件では、ループ変数i
がe2
の長さ(len(e2)
)と「等しいか、または超えた」場合にループを中断します。これにより、i
がlen(e2)
に到達した時点で即座にループが中断されるため、e2
の有効なインデックス範囲外へのアクセスが未然に防がれます。これは、スライスや配列を扱う際のオフバイワンエラーを防ぐための典型的な修正パターンです。
この修正により、s1
がs2
よりも長い場合でも、equal
関数が正しく動作し、不必要なパニックを引き起こすことなく、テストの意図通りに文字列の比較が行われるようになります。
関連リンク
- Go言語の
strings
パッケージドキュメント: https://pkg.go.dev/strings - Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語の
unicode/utf8
パッケージドキュメント: https://pkg.go.dev/unicode/utf8 - Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (コミットメッセージにある
https://golang.org/cl/90110043
はGerritの変更リストへのリンクです)
参考にした情報源リンク
- Go言語公式ドキュメント
- Go言語のソースコード (特に
src/pkg/strings/strings_test.go
) - 一般的なプログラミングにおけるオフバイワンエラーの概念
- Go言語におけるスライスと配列のインデックスに関する知識
- Go言語におけるパニックとエラーハンドリングの基本
- GitHubのコミット履歴と差分表示機能
- Go言語のコードレビュープロセスに関する情報 (LGTM, R, CCなどの略語の意味)