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

[インデックス 15279] ファイルの概要

このコミットは、Go言語の仕様書(doc/go_spec.html)におけるrange式の評価に関する記述を明確にするものです。特に、配列または配列へのポインタをrange式として使用し、かつ最初のイテレーション変数のみが存在する場合に、range式自体が評価されない条件について追記されています。

コミット

commit 61e02ee901e361586291087a986680ee98da6da0
Author: Russ Cox <rsc@golang.org>
Date:   Fri Feb 15 14:39:28 2013 -0500

    spec: clarify when range x does not evaluate x
    
    Fixes #4644.
    
    R=r, adonovan
    CC=golang-dev
    https://golang.org/cl/7307083

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/61e02ee901e361586291087a986680ee98da6da0

元コミット内容

spec: clarify when range x does not evaluate x (仕様書: range xxを評価しない場合を明確化)

変更の背景

Go言語のfor...rangeループにおいて、イテレーションの対象となる式(range式)がいつ評価されるかについて、既存の仕様書の記述が曖昧であったため、その挙動を明確にする必要がありました。特に、配列や配列へのポインタをrange式として使用し、かつインデックスのみ(最初のイテレーション変数)を使用する場合に、range式全体が評価されないという特殊なケースが存在します。この挙動は、例えばrange式が副作用を持つ関数呼び出しである場合に重要となります。このコミットは、この特定のケースにおけるrange式の評価タイミングを明確にすることで、開発者がGoコードの挙動をより正確に理解できるようにすることを目的としています。これは、Go言語のIssue #4644に関連する修正です。

前提知識の解説

  • Go言語のfor...rangeループ: Go言語におけるfor...rangeループは、配列、スライス、文字列、マップ、チャネルなどのコレクションをイテレートするための構文です。基本的な形式はfor index, value := range collection { ... }です。
  • range式の評価: 通常、for...rangeループのrange式は、ループが開始される前に一度だけ評価されます。これにより、ループ中にコレクションが変更されても、イテレーションの対象はループ開始時のスナップショットに基づきます。
  • 配列とスライス:
    • 配列 (Array): 固定長で、同じ型の要素を連続して格納するデータ構造です。Goでは値型であり、変数に代入されるとコピーされます。
    • スライス (Slice): 配列の一部を参照する動的なビューです。スライスは、基となる配列へのポインタ、長さ(len)、容量(cap)の3つの要素から構成されます。
  • ポインタ (Pointer): Goにおけるポインタは、変数のメモリアドレスを指し示します。配列へのポインタは、特定の配列のメモリアドレスを保持します。
  • len()関数: len()は、配列、スライス、マップ、文字列、チャネルの長さを返す組み込み関数です。配列やスライスの場合、要素の数を返します。
  • 定数式 (Constant Expression): コンパイル時に値が決定される式です。例えば、リテラルや、定数のみで構成される演算などが該当します。
  • 副作用 (Side Effect): 関数や式の評価が、その戻り値以外にプログラムの状態を変更する効果を指します。例えば、グローバル変数の変更、I/O操作、パニックの発生などが副作用にあたります。

技術的詳細

このコミットの核心は、Go言語の仕様書におけるfor...rangeループのrange式の評価タイミングに関する記述の修正です。

変更前は、range式はループ開始前に一度だけ評価されるとされていましたが、配列の場合に「式によっては評価されない場合がある」という曖昧な記述がありました。

変更後は、この例外がより具体的に定義されています。 「range式が配列または配列へのポインタであり、かつ最初のイテレーション変数のみが存在する場合(つまり、for i := range xのような形式の場合)、range式の長さ(len(x))のみが評価されます。もしその長さが定義上定数である場合(§Length and capacityのセクションで定義されているように)、range式自体は評価されません。」

これは、以下のようなシナリオで重要になります。

func getArray() [3]int {
    fmt.Println("getArray called")
    return [3]int{1, 2, 3}
}

func main() {
    // 変更前は曖昧だった挙動
    for i := range getArray() { // この場合、getArray()は呼び出されない可能性がある
        _ = i
    }

    // 変更後の明確化
    // getArray()が配列を返すため、かつ最初のイテレーション変数のみが使用されているため、
    // getArray()は呼び出されず、その長さ(3)のみが評価される。
    // これは、配列の長さがコンパイル時に決定される定数であるため。
}

この修正により、range式が副作用を持つ場合(例: 上記のgetArray()のように何かを出力したり、グローバルな状態を変更したりする場合)、その副作用が特定の条件下では発生しないことが明確になります。これは、コンパイラが最適化を行う際に、不要なrange式の評価をスキップできることを意味し、パフォーマンスの向上にも寄与する可能性があります。

また、スライスの場合については、for i := range aのように最初のイテレーション変数のみが存在する場合でも、len(a)までイテレーションが行われ、配列やスライス自体へのインデックスアクセスは行われないという既存の挙動が再確認されています。nilスライスの場合はイテレーション回数が0であることも明記されています。

コアとなるコードの変更箇所

変更はdoc/go_spec.htmlファイル内のrange句に関するセクションで行われています。

--- a/doc/go_spec.html
+++ b/doc/go_spec.html
@@ -4377,9 +4377,15 @@ the range clause is equivalent to the same clause with only the first variable p
 </p>
 
 <p>
-The range expression is evaluated once before beginning the loop
-except if the expression is an array, in which case, depending on
-the expression, it might not be evaluated (see below).
+The range expression is evaluated once before beginning the loop,
+with one exception. If the range expression is an array or a pointer to an array
+and only the first iteration value is present, only the range expression's
+length is evaluated; if that length is constant by definition
+(see §<a href="#Length_and_capacity">Length and capacity</a>),
+the range expression itself will not be evaluated.
+</p>
+
+<p>
 Function calls on the left are evaluated once per iteration.
 For each iteration, iteration values are produced as follows:
 </p>
@@ -4396,8 +4402,8 @@ channel         c  chan E, <-chan E       element  e  E
 <ol>
 <li>
 For an array, pointer to array, or slice value <code>a</code>, the index iteration
-values are produced in increasing order, starting at element index 0. As a special
-case, if only the first iteration variable is present, the range loop produces
+values are produced in increasing order, starting at element index 0.
+If only the first iteration variable is present, the range loop produces
 iteration values from 0 up to <code>len(a)</code> and does not index into the array
 or slice itself. For a <code>nil</code> slice, the number of iterations is 0.
 </li>

コアとなるコードの解説

このコミットでは、主に2つのパラグラフが修正されています。

  1. range式の評価タイミングに関するパラグラフの修正:

    • 変更前: 「range式はループ開始前に一度だけ評価される。ただし、式が配列の場合、その式によっては評価されない場合がある(下記参照)。」
    • 変更後: 「range式はループ開始前に一度だけ評価されるが、一つの例外がある。もしrange式が配列または配列へのポインタであり、かつ最初のイテレーション変数のみが存在する場合、range式の長さのみが評価される。もしその長さが定義上定数である場合(§Length and capacityを参照)、range式自体は評価されない。」 この変更により、range式が評価されない具体的な条件(配列/配列へのポインタ、最初のイテレーション変数のみ、長さが定数)が明確に記述されました。
  2. 配列、配列へのポインタ、スライスのイテレーションに関する箇条書きの修正:

    • 変更前: 「配列、配列へのポインタ、またはスライス値aの場合、インデックスのイテレーション値は要素インデックス0から昇順に生成される。特殊なケースとして、最初のイテレーション変数のみが存在する場合、rangeループは0からlen(a)までのイテレーション値を生成し、配列またはスライス自体をインデックスしない。」
    • 変更後: 「配列、配列へのポインタ、またはスライス値aの場合、インデックスのイテレーション値は要素インデックス0から昇順に生成される。もし最初のイテレーション変数のみが存在する場合rangeループは0からlen(a)までのイテレーション値を生成し、配列またはスライス自体をインデックスしない。」 「特殊なケースとして」という表現が削除され、より一般的な条件として記述されています。これにより、配列やスライスでインデックスのみを使用するfor...rangeループの挙動が、より自然な形で説明されています。また、nilスライスの場合のイテレーション回数が0であることも再確認されています。

これらの変更は、Go言語のfor...rangeループのセマンティクスをより厳密かつ明確に定義し、開発者がコードの挙動を正確に予測できるようにすることを目的としています。

関連リンク

  • Go言語の仕様書: https://golang.org/ref/spec (このコミットで変更されたdoc/go_spec.htmlは、この仕様書の一部です。)
  • Go CL 7307083: https://golang.org/cl/7307083 (このコミットに対応するGoのコードレビューシステムにおけるチェンジリスト)
  • Go Issue #4644: (直接的なリンクは見つかりませんでしたが、コミットメッセージで参照されています。)

参考にした情報源リンク