[インデックス 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などの略語の意味)