Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つの要素で構成されます。

  1. ポインタ: スライスの基底となる配列の要素を指すポインタ。
  2. 長さ (Length): スライスに含まれる要素の数。len(s) で取得できます。
  3. 容量 (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 が省略された形式で、i0 とみなされます。
    • 新しいスライスの長さは j - 0 = j となります。
    • 新しいスライスの容量は k - 0 = k となります。

重要な制約と挙動:

  • 0 <= i <= j <= k <= cap(s) の関係が常に満たされる必要があります。この条件が満たされない場合、ランタイムパニックが発生します。
  • jk は常に指定する必要があります。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言語のコンパイラツールチェインの以下の部分が変更されています。

  1. src/pkg/go/ast/ast.go:

    • 抽象構文木(AST)の SliceExpr 型に新しいフィールド Max Expr が追加されました。これは、3番目のインデックス k を表現するためのものです。
  2. src/pkg/go/ast/walk.go:

    • ASTを走査する Walk 関数が更新され、SliceExprMax フィールドも走査対象に含まれるようになりました。これにより、ASTの完全な走査が可能になります。
  3. src/pkg/go/parser/parser.go:

    • Goソースコードを解析しASTを構築するパーサーの parseIndexOrSlice 関数が大幅に修正されました。
    • スライス式のコロンの数を数えるロジックが追加され、2つ目のコロン(3番目のインデックスの存在)を検出できるようになりました。
    • ast.SliceExpr を構築する際に、LowHigh に加えて Max フィールドも適切に設定されるようになりました。
    • 3インデックススライス式において、2番目と3番目のインデックス(jk)が省略された場合にエラーを報告するロジックが追加されました("2nd and 3rd index must be present full slice expression")。
  4. src/pkg/go/parser/short_test.go:

    • パーサーのテストケースに、新しい3インデックススライス式の有効な構文 (s[i:j:k], s[:j:k]) と無効な構文の例が追加されました。これにより、パーサーが正しく動作することを確認します。
  5. src/pkg/go/printer/nodes.go:

    • ASTからGoソースコードを整形して出力するプリンターの expr1 関数が修正されました。
    • SliceExpr を整形する際に、Max フィールドが存在する場合に3番目のインデックスも出力するロジックが追加されました。
    • コロンの周りの空白の扱いに関するロジックも更新され、3インデックススライス式でも適切なフォーマットが適用されるようになりました。
  6. 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] 形式では jk は省略できないという仕様を反映しています。最終的に、ast.SliceExpr を構築する際に、index[0]Lowindex[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)

	// ... (既存のコード) ...
}

プリンターは、SliceExprMax フィールドが存在するかどうかを確認し、存在する場合は indices スライスに追加します。その後、indices スライスをループ処理することで、LowHighMax の各要素とそれらの間のコロンを適切に整形して出力します。これにより、s[i:j:k] のような形式が正しくGoコードとして再構築されます。コロンの周りの空白の扱いも、既存のルールに則って適用されるようになっています。

これらの変更により、Go言語の構文解析、AST表現、およびコード生成の各段階で、3インデックススライス式が完全にサポートされるようになりました。

関連リンク

参考にした情報源リンク