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

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

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

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

スポンサーリンク

背景

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

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

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

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

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

ForClause を持つ for 文も条件によって制御されるが、加えて代入、インクリメントまたはデクリメント文のような初期化文と後置文を指定することができる。初期化文は短縮変数宣言であってもよいが、後置文はそうであってはならない。

ForClause = [ InitStmt ] ";" [ Condition ] ";" [ PostStmt ] .
InitStmt  = SimpleStmt .
PostStmt  = SimpleStmt .
for i := 0; i < 10; i++ {
	f(i)
}

空でない場合、初期化文は最初の反復のための条件の評価の前に一度だけ実行される。後置文はブロックの各実行の後に(ブロックが実行された場合にのみ)実行される。ForClause のいずれの要素も空にできるが、条件のみの場合を除きセミコロンは必要である。条件が省略された場合、真偽値 true と等価である。

for cond { S() }    は    for ; cond ; { S() }    と同じ
for      { S() }    は    for true     { S() }    と同じ

各反復はそれぞれ独自の個別に宣言された変数(または変数群)を持つ [Go 1.22]。最初の反復で使用される変数は初期化文によって宣言される。後続の各反復で使用される変数は、後置文の実行前に暗黙的に宣言され、その時点での前の反復の変数の値に初期化される。

var prints []func()
for i := 0; i < 5; i++ {
	prints = append(prints, func() { println(i) })
	i++
}
for _, p := range prints {
	p()
}

出力:

1
3
5

Go 1.22 より前では、反復は独自の個別の変数を持つ代わりに1組の変数を共有する。その場合、上記の例は以下を出力する:

6
6
6

解説

3部構成の for 文

C 言語や Java でおなじみの for (init; cond; post) と同じ構造です。

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

3つの部分はそれぞれ以下の役割を持ちます。

部分役割実行タイミング
初期化文(init)ループ変数の準備ループ開始前に1回だけ
条件(cond)続行するかの判定各反復の最初
後置文(post)ループ変数の更新各反復の最後(本体実行後)

各部分は省略できる

3つの部分はどれも省略できますが、セミコロンは残す必要があります。

// 初期化を省略
i := 0
for ; i < 10; i++ {
    fmt.Println(i)
}

// 後置文を省略
for i := 0; i < 10; {
    fmt.Println(i)
    i += 2
}

// 初期化と後置文を省略(条件のみ)→ セミコロンも省略可能
for i < 10 {
    // ...
}

// すべて省略 → 無限ループ
for {
    // ...
}

後置文に := は使えない

初期化文には短縮変数宣言(:=)が使えますが、後置文には使えません。

for i := 0; i < 10; i++ {}       // ✅ 初期化文で := OK
for i := 0; i < 10; j := i++ {} // ❌ 後置文で := はダメ

後置文はループの途中で毎回実行されるので、新しい変数を宣言する場所としてはふさわしくないからです。

Go 1.22 の大きな変更:ループ変数のスコープ

Go 1.22 で、for ループの変数の扱いが大きく変わりました。これは Go の歴史の中でも特に重要な変更です。

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

var prints []func()
for i := 0; i < 5; i++ {
    prints = append(prints, func() { println(i) })
    i++
}
for _, p := range prints {
    p()
}
// 出力: 1, 3, 5(各クロージャが自分専用の i を持つ)

Go 1.21 以前:すべての反復が同じ変数を共有

// 同じコードでも Go 1.21 以前では...
// 出力: 6, 6, 6(すべてのクロージャが同じ i を見ている)

以前はクロージャの中でループ変数を使うと、ループ終了後の最終値が見えてしまうというバグの温床でした。以前の解説で触れた i := i というイディオムが必要だったのはこのためです。

// Go 1.21 以前で必要だった回避策
for i := 0; i < 5; i++ {
    i := i  // ループ変数をシャドーイングしてコピー
    prints = append(prints, func() { println(i) })
}

Go 1.22 以降ではこの回避策は不要です。各反復が自動的に独自の変数を持つので、クロージャの中から参照しても期待どおりの値が得られます。

原文の例を詳しく追う

原文の例は少し複雑なので、ステップごとに見てみましょう。

for i := 0; i < 5; i++ {
    prints = append(prints, func() { println(i) })
    i++  // ループ本体の中でも i をインクリメント
}

後置文の i++ に加えて、本体の中でも i++ しているので、i は毎反復2ずつ増えます。

反復開始時の i本体実行後の i後置文後の i保存される値
1回目01(本体で i++)2(後置で i++)1
2回目2343
3回目456(条件 i<5 で終了)5

Go 1.22 では各反復が独自の i を持つので、クロージャはそれぞれ 135 を出力します。

Go 1.21 以前では全クロージャが同じ i を共有し、ループ終了時の値 6 を出力します。

実用的なパターン

// 基本的なカウントループ
for i := 0; i < len(items); i++ {
    process(items[i])
}

// 逆順ループ
for i := len(items) - 1; i >= 0; i-- {
    process(items[i])
}

// 2つの変数を使うループ
for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
    items[i], items[j] = items[j], items[i]  // 反転
}

ただし、スライスや配列を順に処理するだけなら、次の節で説明される range を使うほうが Go らしい書き方です。

おわりに 

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

よっしー
よっしー

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

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

コメント

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