[インデックス 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/