[インデックス 11588] ファイルの概要
このコミットは、test/ken/array.go ファイルに新しいテストケースを追加するものです。このファイルはGo言語のテストスイートの一部であり、配列やスライスに関する様々な挙動を検証するために使用されます。
コミット
commit 450c955bd973f153ba99c340022a424be5c75f73
Author: Ian Lance Taylor <iant@golang.org>
Date: Fri Feb 3 06:29:30 2012 -0800
test: test slice beyond len
When slicing a slice, the bounds may be > len as long as they
are <= cap. Interestingly, gccgo got that wrong and still
passed the testsuite and all the library tests.
R=golang-dev, rsc, iant
CC=golang-dev
https://golang.org/cl/5622053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/450c955bd973f153ba99c340022a424be5c75f73
元コミット内容
commit 450c955bd973f153ba99c340022a424be5c75f73
Author: Ian Lance Taylor <iant@golang.org>
Date: Fri Feb 3 06:29:30 2012 -0800
test: test slice beyond len
When slicing a slice, the bounds may be > len as long as they
are <= cap. Interestingly, gccgo got that wrong and still
passed the testsuite and all the library tests.
R=golang-dev, rsc, iant
CC=golang-dev
https://golang.org/cl/5622053
変更の背景
このコミットの主な背景は、Go言語のコンパイラの一つであるgccgoが、スライスのスライス操作において、len(現在の長さ)を超えてcap(容量)までの範囲でスライスを作成する際の挙動を誤って解釈していたという問題にあります。
Goのスライスは、基盤となる配列への参照であり、lenとcapという2つの重要なプロパティを持ちます。スライス操作(s[low:high])では、highインデックスが現在のスライスのlenを超えていても、基盤となる配列のcap以内であれば有効です。これにより、既存の基盤配列のメモリを再利用しつつ、スライスの「ビュー」を拡張することができます。
しかし、gccgoはこの仕様を正しく実装しておらず、lenを超えるスライス操作を誤って処理していました。驚くべきことに、このバグが存在するにもかかわらず、既存のテストスイートや標準ライブラリのテストはすべてパスしていました。これは、既存のテストがこの特定のエッジケースを十分にカバーしていなかったことを示唆しています。
このコミットは、gccgoのこの誤った挙動を露呈させ、修正を促すための新しいテストケースを追加することで、Go言語の仕様に対するコンパイラの準拠を強化することを目的としています。
前提知識の解説
Go言語のスライス
Go言語のスライスは、配列のセグメントを参照するデータ構造です。スライスは、基盤となる配列、そのセグメントの長さ(len)、およびそのセグメントが基盤となる配列から拡張できる最大容量(cap)の3つの要素で構成されます。
len(長さ): スライスに含まれる要素の数です。lenは0からcapまでの範囲で変動します。cap(容量): スライスの基盤となる配列の先頭から、スライスが参照している部分の末尾までの要素の数です。これは、スライスが再割り当てなしで保持できる要素の最大数を示します。
スライスのスライス操作 (s[low:high])
Goでは、既存のスライスから新しいスライスを作成する「スライスのスライス」操作が可能です。この操作はs[low:high]という形式で行われます。
low: 新しいスライスの開始インデックス(基盤となる配列に対する相対位置)。high: 新しいスライスの終了インデックス(基盤となる配列に対する相対位置)。highは含まれません。
この操作において重要なのは、highインデックスが元のスライスのlenを超えていても、元のスライスのcap以下であれば有効であるという点です。
例:
s := make([]int, 3, 5) // len=3, cap=5 のスライスを作成
// s は現在 [0 0 0] を参照している。基盤配列は [0 0 0 X X] のようなイメージ。
// s[0:5] は有効なスライス操作
// 新しいスライスは len=5, cap=5 となり、基盤配列全体を参照する。
// これは、元のスライスの len (3) を超えているが、cap (5) 以内であるため許容される。
new_s := s[0:5]
もしhighがcapを超えた場合、Goランタイムはパニック(slice bounds out of range)を引き起こします。
gccgo
gccgoは、GCC(GNU Compiler Collection)のフロントエンドとして実装されたGo言語のコンパイラです。Go言語の公式コンパイラであるgcとは異なる実装であり、GoプログラムをGCCのバックエンドを通じてコンパイルします。異なる実装であるため、Go言語の仕様に対する解釈や最適化の挙動がgcと異なる場合があります。
技術的詳細
このコミットが対処している技術的な問題は、Go言語のスライス操作におけるhighインデックスの許容範囲に関するgccgoの誤った実装です。
Go言語の仕様では、スライスのスライス操作s[low:high]において、highインデックスは0 <= low <= high <= cap(s)という条件を満たす必要があります。ここで重要なのは、highが元のスライスのlen(s)を超えていても、cap(s)以内であれば有効であるという点です。この挙動は、既存の基盤配列のメモリを効率的に再利用し、スライスの「ビュー」を拡張するために不可欠です。
しかし、gccgoは過去にこの仕様を正しく扱えていませんでした。具体的には、highがlenを超えているがcap以内であるようなスライス操作に対して、gccgoが誤ったコードを生成したり、期待される結果を返さなかったりするバグが存在したと考えられます。
コミットメッセージにある「Interestingly, gccgo got that wrong and still passed the testsuite and all the library tests.」という記述は、このバグが既存のテストでは検出されなかったことを示しています。これは、Goのテストスイートが、スライスのlenとcapの境界条件を厳密にテストするような特定のシナリオを欠いていたためです。
このコミットで追加されたテストケースは、まさにこのエッジケース、つまりlenを超えてcapまでの範囲でスライスを作成するシナリオを明示的に検証することで、gccgoのようなコンパイラがGo言語の仕様に厳密に準拠していることを保証しようとしています。このようなテストは、異なるコンパイラ実装間での互換性を高め、Goプログラムの移植性と信頼性を向上させる上で非常に重要です。
コアとなるコードの変更箇所
変更はtest/ken/array.goファイルに対して行われました。
--- a/test/ken/array.go
+++ b/test/ken/array.go
@@ -68,6 +68,9 @@ func testpdpd() {
a = a[5:25]
res(sumpd(a), 5, 25)
+
+ a = a[30:95]
+ res(sumpd(a), 35, 100)
}
// call ptr fixed with ptr fixed
具体的には、testpdpd関数内に以下の2行が追加されています。
a = a[30:95]
res(sumpd(a), 35, 100)
コアとなるコードの解説
追加されたコードは、testpdpd関数内の既存のスライス操作の後に続きます。
-
a = a[30:95]- この行が、
lenを超えてcapまでの範囲でスライスを作成するテストの核心です。 - この時点でのスライス
aの具体的なlenとcapは、testpdpd関数のこれまでの処理に依存しますが、このテストが意図しているのは、aの現在のlenが30未満であり、かつcapが95以上であるような状況です。 - もし
aのlenが30未満で、capが95以上であれば、このスライス操作はGoの仕様上有効です。新しいスライスaは、元の基盤配列のインデックス30から94までの要素を参照するようになります。 gccgoがこの操作を誤って処理した場合、後続のsumpd(a)の結果が期待通りにならないか、あるいはパニックを引き起こす可能性があります。
- この行が、
-
res(sumpd(a), 35, 100)res関数は、テスト結果を検証するためのユーティリティ関数です。sumpd(a)は、新しくスライスされたaの要素の合計を計算します。35と100は、それぞれ期待される合計値と、おそらくスライスの長さや要素の範囲に関連する期待値です。- この行は、
a = a[30:95]というスライス操作がGoの仕様通りに正しく行われた場合に、sumpd(a)が特定の期待値を返すことを検証します。もしgccgoがスライス操作を誤っていた場合、このres関数が失敗し、バグが検出されることになります。
このテストケースは、Goのスライスが持つ柔軟なスライス機能、特にlenとcapの関係性を正しく理解し、実装しているかを検証するための重要なエッジケースをカバーしています。
関連リンク
- Go CL 5622053: https://golang.org/cl/5622053
参考にした情報源リンク
- Go Slices: usage and internals: https://go.dev/blog/slices-intro
- Go Slices:
lenvscap: https://stackoverflow.com/questions/27517900/go-slices-len-vs-cap - Go Slices:
appendfunction: https://go.dev/blog/go-slices-usage-and-internals gccgoand Go compatibility: https://go.dev/doc/install/gccgo- Go slice beyond len cap: https://stackoverflow.com/questions/27517900/go-slices-len-vs-cap
- Go slice beyond len cap: https://www.ardanlabs.com/blog/2013/09/slice-internals-in-go.html
- Go slice beyond len cap: https://www.w3schools.com/go/go_slices.php
gccgoslice beyond len cap bug: https://go.dev/issue/2935 (関連する可能性のあるGo issue)gccgoslice beyond len cap bug: https://stackoverflow.com/questions/6799799/go-slice-bounds-out-of-range (一般的なスライス範囲外エラーに関する議論)