[インデックス 16612] ファイルの概要
このコミットは、Go言語におけるスライス式に3番目のインデックス(cap、容量)を指定する機能、すなわち s[:j:k] および s[i:j:k] のサポートを追加するものです。これにより、新しいスライスを作成する際に、その長さだけでなく容量も明示的に制御できるようになります。これは、既存のスライスの基底配列の容量を不必要に公開することなく、より安全で予測可能なスライス操作を可能にするための重要な変更です。
コミット
commit f243584d297e183112cc97fcc09a9140961dc188
Author: Robert Griesemer <gri@golang.org>
Date: Fri Jun 21 13:14:06 2013 -0700
go/*: support for slices with cap: s[:j:k] and s[i:j:k]
Experimental, per rsc's proposal.
R=rsc
CC=golang-dev
https://golang.org/cl/10204043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f243584d297e183112cc97fcc09a9140961dc188
元コミット内容
Go言語の構文解析器(parser)と抽象構文木(AST)、およびコード整形器(printer)に、スライス式で容量(capacity)を指定する新しい形式 s[:j:k] および s[i:j:k] のサポートを追加します。これは、Robert Griesemer氏によって提案された実験的な機能です。
変更の背景
Go言語のスライスは、基底となる配列の一部を参照するデータ構造です。従来のスライス式 s[i:j] では、新しいスライスの長さは j-i で決まりますが、容量は基底配列の i から終端までの残りの要素数に設定されていました。この挙動は、多くの場合で便利ですが、特定のシナリオでは意図しない副作用を引き起こす可能性がありました。
例えば、あるスライスから部分スライスを作成し、その部分スライスに append 操作を行った場合、元のスライスの容量が大きいために、append によって元のスライスの基底配列のデータが上書きされてしまうことがありました。これは、特に複数のスライスが同じ基底配列を共有している場合に、予期せぬデータの変更につながる可能性がありました。
この問題を解決し、スライスの容量をより細かく制御できるようにするために、Russ Cox (rsc) 氏の提案に基づき、3番目のインデックス k を導入する実験的な機能が検討されました。この k を指定することで、新しいスライスの容量を k-i に制限し、append 操作による基底配列の意図しない上書きを防ぐことが可能になります。このコミットは、そのためのGo言語のツールチェイン(AST、パーサー、プリンター)への変更を実装したものです。
前提知識の解説
Go言語のスライス
Go言語のスライスは、配列をラップした動的なビューです。スライスは以下の3つの要素で構成されます。
- ポインタ: スライスの基底となる配列の要素を指すポインタ。
- 長さ (Length): スライスに含まれる要素の数。
len(s)で取得できます。 - 容量 (Capacity): スライスの基底となる配列の、スライスが参照している開始位置から終端までの要素の最大数。
cap(s)で取得できます。
スライスは、make([]T, length, capacity) で作成するか、既存の配列やスライスからスライス式を使って作成します。
2インデックススライス式 s[i:j]
Go言語で最も一般的なスライス式は s[i:j] です。
i: 新しいスライスの開始インデックス(inclusive)。省略された場合は0となります。j: 新しいスライスの終了インデックス(exclusive)。省略された場合は元のスライスの長さlen(s)となります。
この式で作成される新しいスライスの特性は以下の通りです。
- 長さ:
j - i - 容量:
cap(s) - i(元のスライスの容量から開始インデックスまでの要素数を引いたもの)
例:
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s := arr[2:5] // sは [2 3 4]
// len(s) は 3 (5 - 2)
// cap(s) は 8 (10 - 2)
この場合、s の容量は8なので、append 操作によって s に要素を追加すると、基底配列の arr[5] 以降の要素が上書きされる可能性があります。
append 関数と容量
append 関数は、スライスに要素を追加するために使用されます。
- スライスの容量に余裕がある場合、
appendは既存の基底配列の空き領域に要素を追加し、スライスの長さを更新します。 - スライスの容量が不足している場合、
appendはより大きな新しい基底配列を割り当て、既存の要素をコピーし、新しい要素を追加します。このとき、元のスライスとは異なる基底配列を参照するようになります。
3インデックススライスは、この append の挙動をより予測可能にし、意図しないデータの上書きを防ぐために導入されました。
技術的詳細
このコミットで導入された3インデックススライス式 s[i:j:k] および s[:j:k] は、新しいスライスの容量を明示的に指定することを可能にします。
-
s[i:j:k]:i: 新しいスライスの開始インデックス(inclusive)。省略された場合は0となります。j: 新しいスライスの終了インデックス(exclusive)。これにより、新しいスライスの長さがj - iとなります。k: 新しいスライスの容量の終了インデックス(exclusive)。これにより、新しいスライスの容量がk - iとなります。
-
s[:j:k]:iが省略された形式で、iは0とみなされます。- 新しいスライスの長さは
j - 0 = jとなります。 - 新しいスライスの容量は
k - 0 = kとなります。
重要な制約と挙動:
0 <= i <= j <= k <= cap(s)の関係が常に満たされる必要があります。この条件が満たされない場合、ランタイムパニックが発生します。jとkは常に指定する必要があります。2インデックススライスのように省略することはできません。- この機能の主な目的は、新しいスライスの容量を制限することです。これにより、そのスライスに対する
append操作が、元の基底配列の他の部分に影響を与えることを防ぎます。容量が制限されたスライスにappendし、その容量を超過する場合、Goランタイムは必ず新しい基底配列を割り当てます。
例:
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 2インデックススライス
s1 := arr[2:5]
// len(s1) = 3, cap(s1) = 8 (arr[2]からarrの終わりまで)
// 3インデックススライス
s2 := arr[2:5:7]
// len(s2) = 3 (5 - 2)
// cap(s2) = 5 (7 - 2)
// s2にappendして容量を超えると、arr[7]以降は上書きされない
コアとなるコードの変更箇所
このコミットでは、Go言語のコンパイラツールチェインの以下の部分が変更されています。
-
src/pkg/go/ast/ast.go:- 抽象構文木(AST)の
SliceExpr型に新しいフィールドMax Exprが追加されました。これは、3番目のインデックスkを表現するためのものです。
- 抽象構文木(AST)の
-
src/pkg/go/ast/walk.go:- ASTを走査する
Walk関数が更新され、SliceExprのMaxフィールドも走査対象に含まれるようになりました。これにより、ASTの完全な走査が可能になります。
- ASTを走査する
-
src/pkg/go/parser/parser.go:- Goソースコードを解析しASTを構築するパーサーの
parseIndexOrSlice関数が大幅に修正されました。 - スライス式のコロンの数を数えるロジックが追加され、2つ目のコロン(3番目のインデックスの存在)を検出できるようになりました。
ast.SliceExprを構築する際に、Low、Highに加えてMaxフィールドも適切に設定されるようになりました。- 3インデックススライス式において、2番目と3番目のインデックス(
jとk)が省略された場合にエラーを報告するロジックが追加されました("2nd and 3rd index must be present full slice expression")。
- Goソースコードを解析しASTを構築するパーサーの
-
src/pkg/go/parser/short_test.go:- パーサーのテストケースに、新しい3インデックススライス式の有効な構文 (
s[i:j:k],s[:j:k]) と無効な構文の例が追加されました。これにより、パーサーが正しく動作することを確認します。
- パーサーのテストケースに、新しい3インデックススライス式の有効な構文 (
-
src/pkg/go/printer/nodes.go:- ASTからGoソースコードを整形して出力するプリンターの
expr1関数が修正されました。 SliceExprを整形する際に、Maxフィールドが存在する場合に3番目のインデックスも出力するロジックが追加されました。- コロンの周りの空白の扱いに関するロジックも更新され、3インデックススライス式でも適切なフォーマットが適用されるようになりました。
- ASTからGoソースコードを整形して出力するプリンターの
-
src/pkg/go/printer/testdata/expressions.golden,src/pkg/go/printer/testdata/expressions.input,src/pkg/go/printer/testdata/expressions.raw:- プリンターのテストデータに、3インデックススライス式の様々なパターンが追加されました。これにより、プリンターが新しい構文を正しく整形できることを確認します。
コアとなるコードの解説
src/pkg/go/ast/ast.go の変更
type (
SliceExpr struct {
Lbrack token.Pos // position of "["
Low Expr // begin of slice range; or nil
High Expr // end of slice range; or nil
Max Expr // maximum capacity of slice; or nil // <-- 追加
Rbrack token.Pos // position of "]"
}
)
SliceExpr 構造体に Max フィールドが追加されたことで、ASTレベルでスライスの容量指定を表現できるようになりました。これは、パーサーがソースコードからこの情報を抽出し、プリンターがASTからソースコードを再構築する際の基盤となります。
src/pkg/go/parser/parser.go の変更
parseIndexOrSlice 関数は、インデックス式またはスライス式を解析するGoパーサーの核心部分です。
func (p *parser) parseIndexOrSlice(x ast.Expr) ast.Expr {
// ... (既存のコード) ...
lbrack := p.expect(token.LBRACK)
p.exprLev++
var index [3]ast.Expr // change the 3 to 2 to disable slice expressions w/ cap
if p.tok != token.COLON {
index[0] = p.parseRhs() // 最初のインデックス (i)
}
ncolons := 0
for p.tok == token.COLON && ncolons < len(index)-1 {
p.next() // コロンを消費
ncolons++
if p.tok != token.COLON && p.tok != token.RBRACK && p.tok != token.EOF {
index[ncolons] = p.parseRhs() // 2番目または3番目のインデックス (j または k)
}
}
p.exprLev--
rbrack := p.expect(token.RBRACK)
if ncolons > 0 { // スライス式の場合
// slice expression
if ncolons == 2 && (index[1] == nil || index[2] == nil) {
// only i is optional in a[i:j:k]
p.error(rbrack, "2nd and 3rd index must be present full slice expression")
}
return &ast.SliceExpr{X: x, Lbrack: lbrack, Low: index[0], High: index[1], Max: index[2], Rbrack: rbrack}
}
// ... (インデックス式の場合の既存のコード) ...
}
この変更の核心は、index [3]ast.Expr という配列を導入し、コロンの数を ncolons でカウントすることで、スライス式の形式を判別している点です。
ncolons == 0:s[i]のようなインデックス式。ncolons == 1:s[i:j]のような2インデックススライス式。ncolons == 2:s[i:j:k]のような3インデックススライス式。
特に、ncolons == 2 の場合、つまり3インデックススライス式の場合に index[1] (j) または index[2] (k) が nil であればエラーを発生させています。これは、s[i:j:k] 形式では j と k は省略できないという仕様を反映しています。最終的に、ast.SliceExpr を構築する際に、index[0] を Low、index[1] を High、そして新しく index[2] を Max として割り当てています。
src/pkg/go/printer/nodes.go の変更
printer はASTをGoコードに変換する役割を担います。
func (p *printer) expr1(expr ast.Expr, prec1, depth int) {
// ... (既存のコード) ...
case *ast.SliceExpr:
// ... (既存のコード) ...
p.expr1(x.X, token.HighestPrec, 1)
p.print(x.Lbrack, token.LBRACK)
indices := []ast.Expr{x.Low, x.High}
if x.Max != nil { // <-- Maxフィールドが存在する場合
indices = append(indices, x.Max) // indicesにMaxを追加
}
for i, y := range indices {
if i > 0 {
// blanks around ":" if both sides exist and either side is a binary expression
// TODO(gri) once we have committed a variant of a[i:j:k] we may want to fine-
// tune the formatting here
x := indices[i-1]
if depth <= 1 && x != nil && y != nil && (isBinary(x) || isBinary(y)) {
p.print(blank, token.COLON, blank)
} else {
p.print(token.COLON)
}
}
if y != nil {
p.expr0(y, depth+1)
}
}
p.print(x.Rbrack, token.RBRACK)
// ... (既存のコード) ...
}
プリンターは、SliceExpr の Max フィールドが存在するかどうかを確認し、存在する場合は indices スライスに追加します。その後、indices スライスをループ処理することで、Low、High、Max の各要素とそれらの間のコロンを適切に整形して出力します。これにより、s[i:j:k] のような形式が正しくGoコードとして再構築されます。コロンの周りの空白の扱いも、既存のルールに則って適用されるようになっています。
これらの変更により、Go言語の構文解析、AST表現、およびコード生成の各段階で、3インデックススライス式が完全にサポートされるようになりました。
関連リンク
- Go CL 10204043: https://golang.org/cl/10204043
参考にした情報源リンク
- Go Slices: usage and internals: https://go.dev/blog/slices-intro
- Three-index slice expressions in Go: https://ardanlabs.com/blog/2013/09/three-index-slice-expressions-in-go.html
- Go Slices Explained: https://golangprojectstructure.com/go-slices-explained/