Go言語入門:言語仕様 -Vol.115-

スポンサーリンク
Go言語入門:言語仕様 -Vol.115- 用語解説
Go言語入門:言語仕様 -Vol.115-
この記事は約14分で読めます。
よっしー
よっしー

こんにちは。よっしーです(^^)

本日は、Go言語の言語仕様について解説しています。

スポンサーリンク

背景

Go言語を学び始めて、公式の「The Go Programming Language Specification(言語仕様書)」を開いてみたものの、「英語で書かれていて読むのが大変…」「専門用語ばかりで何を言っているのかわからない…」と感じたことはありませんか? 実は、多くのGo初心者が同じ壁にぶつかっています。

言語仕様書は、Go言語の「正式な取扱説明書」のような存在です。プログラミング言語がどのように動くのか、どんなルールで書くべきなのかが詳しく書かれていますが、その分、初めて読む人には難しく感じられるのも事実です。

そこでこの記事では、言語仕様書の導入部分を丁寧な日本語訳とともに、初心者の方でも理解しやすい補足説明を加えてお届けします。「強く型付けされている」「ガベージコレクション」「並行プログラミング」といった専門用語も、具体例を交えながらわかりやすく解説していきます。

言語仕様書は難しそうに見えますが、一つひとつの概念を丁寧に読み解いていけば、必ず理解できます。一緒に、Go言語の基礎をしっかり学んでいきましょう!

range 節を持つ for 文(For statements with range clause)

range 節を持つ for 文は、配列、スライス、文字列、またはmapのすべてのエントリ、チャネルで受信される値、ゼロから上限までの整数値 [Go 1.22]、またはイテレータ関数の yield 関数に渡される値 [Go 1.23] を反復処理する。各エントリに対して、反復変数が存在すれば対応する反復変数に反復値を代入し、その後ブロックを実行する。

RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .

range 節の右辺の式はレンジ式と呼ばれ、配列、配列へのポインタ、スライス、文字列、map、受信操作を許可するチャネル、整数、または特定のシグネチャを持つ関数(以下を参照)であってよい。代入と同様に、左辺のオペランドが存在する場合、アドレス指定可能であるかmapのインデックス式でなければならない。これらは反復変数を表す。レンジ式が関数の場合、反復変数の最大数は関数のシグネチャに依存する。レンジ式がチャネルまたは整数の場合、反復変数は最大1つ許可される。それ以外の場合、最大2つまで許可される。最後の反復変数がブランク識別子の場合、range 節はその識別子のない同じ節と等価である。

レンジ式 x はループの開始前に評価される。ただし1つの例外がある:反復変数が最大1つ存在し、x または len(x) が定数の場合、レンジ式は評価されない。

左辺の関数呼び出しは反復ごとに一度評価される。各反復において、対応する反復変数が存在する場合、反復値は以下のように生成される:

レンジ式                                                1番目の値               2番目の値

配列またはスライス  a  [n]E, *[n]E, or []E             インデックス i  int     a[i]       E
文字列              s  文字列型                         インデックス i  int     以下参照   rune
map                 m  map[K]V                         キー         k  K       m[k]       V
チャネル            c  chan E, <-chan E                 要素         e  E
整数値              n  整数型、または型なし int          値           i  以下参照
関数, 0値           f  func(func() bool)
関数, 1値           f  func(func(V) bool)              値           v  V
関数, 2値           f  func(func(K, V) bool)           キー         k  K       v          V

配列、配列へのポインタ、またはスライス値 a に対して、インデックスの反復値は要素インデックス0から始まる昇順で生成される。反復変数が最大1つ存在する場合、range ループは 0 から len(a)-1 までの反復値を生成し、配列やスライス自体へのインデックスアクセスは行わない。nil スライスの場合、反復回数は0である。

文字列値に対して、range 節はバイトインデックス0から始まる文字列内の Unicode コードポイントを反復する。連続する反復において、インデックス値は文字列内の連続する UTF-8 エンコードされたコードポイントの最初のバイトのインデックスであり、rune 型の2番目の値は対応するコードポイントの値である。反復が無効な UTF-8 シーケンスに遭遇した場合、2番目の値は 0xFFFD(Unicode 置換文字)となり、次の反復は文字列内の1バイト分だけ進む。

mapの反復順序は規定されておらず、ある反復から次の反復まで同じであることは保証されない。まだ到達していないmapエントリが反復中に削除された場合、対応する反復値は生成されない。反復中にmapエントリが作成された場合、そのエントリは反復中に生成されるかスキップされる可能性がある。この選択は作成されたエントリごと、および反復ごとに異なりうる。mapが nil の場合、反復回数は0である。

チャネルに対して、生成される反復値はチャネルが閉じられるまでチャネルで送信された連続する値である。チャネルが nil の場合、レンジ式は永遠にブロックする。

整数型または型なし整数定数である整数値 n に対して、反復値 0 から n-1 が昇順に生成される。n が整数型の場合、反復値はその同じ型を持つ。そうでなければ、n の型は反復変数に代入されるかのように決定される。具体的には:反復変数が既存の場合、反復値の型は反復変数の型であり、整数型でなければならない。そうでなければ、反復変数が range 節によって宣言されるか存在しない場合、反復値の型は n のデフォルト型である。n <= 0 の場合、ループは反復を実行しない。

関数 f に対して、反復は新しく合成された yield 関数を引数として f を呼び出すことで進行する。f が戻る前に yield が呼び出されると、yield の引数がループ本体を一度実行するための反復値となる。連続する各ループ反復の後、yield は true を返し、ループを続行するために再度呼び出すことができる。ループ本体が終了しない限り、range 節は f が戻るまで各 yield 呼び出しに対してこの方法で反復値を生成し続ける。ループ本体が終了した場合(break 文などにより)、yield は false を返し、再度呼び出してはならない。

反復変数は短縮変数宣言(:=)の形式を使用して range 節によって宣言することができる。この場合、そのスコープは for 文のブロックであり、各反復はそれぞれ独自の新しい変数を持つ [Go 1.22](ForClause を持つ for 文も参照)。変数はそれぞれの反復値の型を持つ。

反復変数が range 節によって明示的に宣言されない場合、既存のものでなければならない。この場合、反復値は代入文と同様に対応する変数に代入される。

var testdata *struct {
	a *[7]int
}
for i, _ := range testdata.a {
	// testdata.a は決して評価されない; len(testdata.a) は定数
	// i は 0 から 6 の範囲
	f(i)
}

var a [10]string
for i, s := range a {
	// i の型は int
	// s の型は string
	// s == a[i]
	g(i, s)
}

var key string
var val interface{}  // m の要素型は val に代入可能
m := map[string]int{"mon":0, "tue":1, "wed":2, "thu":3, "fri":4, "sat":5, "sun":6}
for key, val = range m {
	h(key, val)
}
// key == 反復で最後に遭遇したmapキー
// val == map[key]

var ch chan Work = producer()
for w := range ch {
	doWork(w)
}

// チャネルを空にする
for range ch {}

// f(0), f(1), ... f(9) を呼び出す
for i := range 10 {
	// i の型は int(型なし定数 10 のデフォルト型)
	f(i)
}

// 不正: 256 は uint8 に代入できない
var u uint8
for u = range 256 {
}

// 不正: 1e3 は浮動小数点定数
for range 1e3 {
}

// fibo はフィボナッチ数列を生成する
fibo := func(yield func(x int) bool) {
	f0, f1 := 0, 1
	for yield(f0) {
		f0, f1 = f1, f0+f1
	}
}

// 1000未満のフィボナッチ数を出力する:
for x := range fibo {
	if x >= 1000 {
		break
	}
	fmt.Printf("%d ", x)
}
// 出力: 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987

// 再帰的な木データ構造のための反復サポート
type Tree[K cmp.Ordered, V any] struct {
	left, right *Tree[K, V]
	key         K
	value       V
}

func (t *Tree[K, V]) walk(yield func(key K, val V) bool) bool {
	return t == nil || t.left.walk(yield) && yield(t.key, t.value) && t.right.walk(yield)
}

func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) {
	t.walk(yield)
}

// 木 t を通順で走査する
var t Tree[string, int]
for k, v := range t.Walk {
	// k, v を処理する
}

レンジ式の型が型パラメータの場合、その型集合のすべての型は同じ基底型を持ち、レンジ式はその型に対して有効でなければならない。または、型集合がチャネル型を含む場合、同一の要素型を持つチャネル型のみを含まなければならず、すべてのチャネル型は受信操作を許可しなければならない。


解説

range の全体像

range は、Go でコレクションを順に処理するための構文です。対象の型によって得られる値が変わります。

// スライス:インデックスと値
for i, v := range []int{10, 20, 30} {
    fmt.Println(i, v)  // 0 10, 1 20, 2 30
}

// 文字列:バイトインデックスとルーン
for i, ch := range "Go言語" {
    fmt.Printf("%d: %c\n", i, ch)  // 0:G, 1:o, 2:言, 5:語
}

// map:キーと値
for k, v := range map[string]int{"a": 1, "b": 2} {
    fmt.Println(k, v)
}

// チャネル:受信した値
for v := range ch {
    fmt.Println(v)
}

// 整数(Go 1.22):0 から n-1
for i := range 5 {
    fmt.Println(i)  // 0, 1, 2, 3, 4
}

// イテレータ関数(Go 1.23)
for v := range iterFunc {
    fmt.Println(v)
}

不要な変数は省略できる

反復変数は省略したり、_ で捨てたりできます。

// 値だけ必要(インデックス不要)
for _, v := range items {
    process(v)
}

// インデックスだけ必要
for i := range items {
    fmt.Println(i)
}

// どちらも不要(反復回数だけ必要)
for range items {
    count++
}

文字列の range:バイトインデックスに注意

文字列を range するとき、1番目の値はバイトインデックスであり、文字のインデックスではありません。

s := "Go言語"
for i, ch := range s {
    fmt.Printf("バイト位置 %d: %c\n", i, ch)
}
// バイト位置 0: G
// バイト位置 1: o
// バイト位置 2: 言  ← 2 であって 2,3,4 番目のバイトにまたがる
// バイト位置 5: 語  ← 3 ではなく 5(「言」が3バイト占めるため)

range は自動的に UTF-8 をデコードして rune(文字)単位で処理してくれます。for i := 0; i < len(s); i++ のようにバイト単位でアクセスすると文字が壊れるので、文字列を文字単位で処理したいときは必ず range を使いましょう。

map の range:順序は不定

map の反復順序はランダムであり、毎回異なる可能性があります。

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v)
}
// 実行するたびに順序が変わる可能性がある

これは意図的な設計です。順序に依存するコードを書かせないためです。順序が必要な場合は、キーをソートしてからアクセスします。

keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
    fmt.Println(k, m[k])
}

また、反復中にmapを変更する(エントリの追加や削除)と、その変更が反復に反映されるかどうかは不定です。安全のため、反復中のmap変更は避けるか、慎重に行いましょう。

整数の range(Go 1.22)

Go 1.22 で追加された便利な機能です。range N で 0 から N-1 まで繰り返せます。

// 従来の書き方
for i := 0; i < 10; i++ {
    f(i)
}

// Go 1.22 以降
for i := range 10 {
    f(i)
}

単純な繰り返しがすっきり書けるようになりました。ただし、浮動小数点数は使えません。

for range 1e3 {  // コンパイルエラー! 1e3 は浮動小数点定数
}

for range 1000 {  // OK! 整数定数
}

イテレータ関数(Go 1.23)

Go 1.23 で追加された最も新しい機能です。func(func(...) bool) というシグネチャの関数を range の対象にできます。

仕組みを理解するために、フィボナッチ数列の例を段階的に見てみましょう。

fibo := func(yield func(x int) bool) {
    f0, f1 := 0, 1
    for yield(f0) {        // yield を呼ぶと値が「生成」される
        f0, f1 = f1, f0+f1 // 次の値を計算
    }
}

この関数は yield を繰り返し呼び出し、呼び出すたびに range ループの1回の反復が実行されます。

for x := range fibo {
    if x >= 1000 {
        break  // break すると yield が false を返し、fibo のループも終了
    }
    fmt.Printf("%d ", x)
}

yield の戻り値が bool なのがポイントです。ループ本体が break なしで終わると yieldtrue を返し、fibo は次の値を生成できます。break が実行されると yieldfalse を返し、fibo のループも終了します。

イテレータ関数には3つのシグネチャがあります。

// 値なし(反復回数だけ意味がある)
func(func() bool)

// 値1つ
func(func(V) bool)

// 値2つ(キーと値)
func(func(K, V) bool)

木構造の走査のような再帰的なデータ構造の反復も、イテレータで自然に書けます。

// 木を通順で走査するイテレータ
func (t *Tree[K, V]) Walk(yield func(key K, val V) bool) {
    t.walk(yield)
}

// 使う側はシンプル
for k, v := range t.Walk {
    fmt.Println(k, v)
}

レンジ式の評価タイミング

レンジ式はループ開始前に一度だけ評価されます。

s := []int{1, 2, 3, 4, 5}
for i, v := range s {
    fmt.Println(i, v)
    s = nil  // s を nil にしても、range は元のスライスの値を使い続ける
}
// 5回すべて実行される

ただし、配列の長さが定数で反復変数が最大1つの場合は、レンジ式自体が評価されないことがあります。原文の testdata.a の例がこれに当たります。testdatanil でも、len(testdata.a) は定数(7)なので、testdata.a は評価されずパニックしません。

Go 1.22 以降:各反復が独自の変数を持つ

前の節で学んだとおり、Go 1.22 以降は range でも各反復が独自の変数を持ちます。

var funcs []func()
for _, v := range []int{1, 2, 3} {
    funcs = append(funcs, func() { fmt.Println(v) })
}
for _, f := range funcs {
    f()
}
// Go 1.22 以降: 1, 2, 3
// Go 1.21 以前: 3, 3, 3

クロージャの落とし穴を心配する必要がなくなりました。

おわりに 

本日は、Go言語の言語仕様について解説しました。

よっしー
よっしー

何か質問や相談があれば、コメントをお願いします。また、エンジニア案件の相談にも随時対応していますので、お気軽にお問い合わせください。

それでは、また明日お会いしましょう(^^)

コメント

タイトルとURLをコピーしました