Go言語はシンプルさを追求した設計思想を持っており、その哲学は繰り返し処理の構文にも色濃く反映されています。

他のプログラミング言語では forwhiledo-whileforeach といった複数のループ構文が用意されていることが一般的です。

しかし、Go言語において繰り返しを制御する予約語はforただ一つしか存在しません。

このシンプルさはコードの可読性を高めるだけでなく、エンジニアが迷う余地を減らすという大きなメリットをもたらします。

本記事では、Go言語の for 文の基本から、スライスやマップの反復処理、そして近年のアップデートで導入された革新的なイテレータ(Iterator)の仕組みまでを詳しく解説します。

Goにおける唯一の繰り返し構文「for」の基本

Go言語の for 文は、大きく分けて3つの記述スタイルが存在します。

最も馴染み深いのは、初期化、条件式、後処理を記述する伝統的なスタイルでしょう。

3つの構成要素を持つ標準的な記述

C言語系から受け継がれたこのスタイルは、指定した回数だけループを回す際や、特定のインデックスを操作する際に適しています。

丸括弧 () を必要としないのがGo言語の特徴です。

Go
package main

import "fmt"

func main() {
    // i := 0 (初期化); i < 5 (条件); i++ (後処理)
    for i := 0; i < 5; i++ {
        fmt.Printf("%d回目のループです\n", i)
    }
}
実行結果
0回目のループです
1回目のループです
2回目のループです
3回目のループです
4回目のループです

この形式では、ループ内で使用するカウンタ変数のスコープが for ブロック内に限定されます。

そのため、ループ終了後に誤って変数 i を参照する心配がなく、安全なコーディングが可能です。

条件式のみを記述するスタイル(While文の代替)

Go言語には while キーワードがありませんが、for 文に条件式だけを記述することで同様の動作を実現できます。

特定の条件が満たされている間だけ処理を継続したい場合に非常に便利です。

Go
package main

import "fmt"

func main() {
    count := 1
    // 条件式だけを記述。countが3以下の間ループを継続する
    for count <= 3 {
        fmt.Println("カウント:", count)
        count++
    }
}
実行結果
カウント: 1
カウント: 2
カウント: 3

この書き方は、読み込み対象のデータがなくなるまで処理を続けたいストリーム処理などで多用されます。

無限ループと制御

条件式を一切記述しない for 文は、無限ループとして機能します。

サーバープログラムのメインループや、特定のイベントを待ち受ける処理で利用されます。

Go
package main

import "fmt"

func main() {
    i := 0
    for {
        if i >= 3 {
            // ループを脱出する
            break
        }
        fmt.Println("無限ループ内の処理:", i)
        i++
    }
}
実行結果
無限ループ内の処理: 0
無限ループ内の処理: 1
無限ループ内の処理: 2

無限ループを使用する際は、必ず内部に終了条件を設け、breakなどで適切に制御する必要があります。

rangeキーワードによるコレクションの反復処理

Go言語で最も頻繁に利用されるのが、range キーワードを組み合わせた for ループです。

これは配列、スライス、マップ、文字列、チャネルの要素を順番に取得するための非常に強力な仕組みです。

スライスと配列の走査

スライスを range で回すと、インデックスと要素の値のペアを取得できます。

Go
package main

import "fmt"

func main() {
    fruits := []string{"apple", "banana", "cherry"}

    // インデックス(i)と要素の値(v)を取得
    for i, v := range fruits {
        fmt.Printf("インデックス: %d, 値: %s\n", i, v)
    }
}
実行結果
インデックス: 0, 値: apple
インデックス: 1, 値: banana
インデックス: 2, 値: cherry

もしインデックスが必要ない場合は、ブランク識別子( _ )を使用して無視することが推奨されます。

Go
for _, v := range fruits {
    fmt.Println("値のみ取得:", v)
}

マップ(連想配列)の操作と順序の注意点

マップを range で反復処理する場合、キーと値を取得できます。

ここで非常に重要なのは、マップの反復順序は実行ごとに変わる可能性があるという点です。

Go
package main

import "fmt"

func main() {
    scores := map[string]int{"Alice": 90, "Bob": 80, "Charlie": 70}

    for name, score := range scores {
        fmt.Printf("%sさんの得点: %d\n", name, score)
    }
}

Goの設計上、マップの順序に依存したコードを記述することはアンチパターンとされています。

順序を固定したい場合は、一度キーをスライスに抽出してソートしてからループを回す必要があります。

文字列(string)のループ処理

文字列を range で処理する場合、バイト単位ではなく Unicode(rune) 単位で反復が行われます。

これにより、日本語のようなマルチバイト文字も正しく一文字ずつ扱うことができます。

Go
package main

import "fmt"

func main() {
    text := "Go言語"

    for i, r := range text {
        // %c は文字として出力、%d はバイト位置を出力
        fmt.Printf("位置: %d, 文字: %c\n", i, r)
    }
}
実行結果
位置: 0, 文字: G
位置: 1, 文字: o
位置: 2, 文字: 言
位置: 5, 文字: 語

「言」の次の文字が「5」から始まっている点に注目してください。

これは「言」という文字が UTF-8 で3バイトを占有しているためです。

Go 1.22/1.23から始まったイテレータの革命

2024年から2026年にかけて、Go言語の for 文には歴史的な改善と新機能が導入されました。

これにより、大規模なデータ構造の処理やカスタムコレクションの反復が劇的に記述しやすくなりました。

範囲変数のスコープ仕様変更

Go 1.22において、長年エンジニアを悩ませてきた「ループ変数の参照」に関する挙動が修正されました。

以前の仕様では、ループ内で定義された変数のアドレスをクロージャなどで保持すると、全てのクロージャが最後の要素を参照してしまう問題がありました。

現在のGoでは、各ループのステップごとに新しい変数が生成されるため、直感に反するバグを未然に防げるようになっています。

iterパッケージの導入とイテレータ関数

Go 1.23で正式に導入された iter パッケージは、言語標準のイテレータインターフェースを定義しました。

これにより、独自のデータ構造に対して range ループを適用できる仕組みであるrange-over-funcが利用可能になりました。

これまでは、独自のリスト構造を走査するためには Next() メソッドを手動で呼び出す必要がありましたが、最新のGoでは for-range にそのまま渡せます。

range-over-funcによるカスタム反復の実装

自分で定義した関数を for-range で回すには、特定のシグネチャを持つ関数(イテレータ)を定義します。

例えば、iter.Seq[V] は、一つの値 V を返すイテレータを表現します。

Go
package main

import (
    "fmt"
    "iter"
)

// 0からnまでの偶数を返すカスタムイテレータ
func EvenNumbers(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i <= n; i++ {
            if i%2 == 0 {
                // yieldを呼び出して値を渡す。戻り値がfalseなら中断
                if !yield(i) {
                    return
                }
            }
        }
    }
}

func main() {
    // 独自に定義した関数をrangeで回すことができる
    for num := range EvenNumbers(10) {
        fmt.Printf("%d ", num)
    }
}
実行結果
0 2 4 6 8 10

yield はコールバック関数であり、呼び出し側のループ本体を実行する役割を果たします。

yieldfalse を返した場合は、ループ側で break が発生したことを意味するため、即座に関数を終了させる実装にします。

この仕組みにより、メモリ上に全ての要素を展開することなく、必要な分だけ計算して返す「遅延評価」のような処理が標準的に書けるようになりました。

ループ制御とラベルの活用

複雑なロジックを記述する際、ループの流れを柔軟に制御する必要があります。

Goには breakcontinue に加え、多段ループを一気に抜けるためのラベル機能が備わっています。

breakとcontinueの使い分け

break は現在のループを完全に終了させます。

対して continue は、現在の回の処理をスキップし、次の反復(または後処理)へ進みます。

Go
for i := 1; i <= 5; i++ {
    if i == 2 {
        continue // 2のときは出力せず次へ
    }
    if i == 4 {
        break // 4のときにループ自体を終了
    }
    fmt.Println(i)
}
実行結果
1
3

ネストされたループを抜けるラベル指定

二重、三重になったループの中から、一気に最外周のループを抜けたい場合があります。

このようなケースでは、ラベルを使用することで break 先を明示的に指定できます。

Go
package main

import "fmt"

func main() {
OuterLoop: // ラベルの定義
    for i := 1; i <= 3; i++ {
        for j := 1; j <= 3; j++ {
            if i == 2 && j == 2 {
                fmt.Println("条件一致: 全てのループを抜けます")
                break OuterLoop
            }
            fmt.Printf("i:%d, j:%d\n", i, j)
        }
    }
}
実行結果
i:1, j:1
i:1, j:2
i:1, j:3
i:2, j:1
条件一致: 全てのループを抜けます

ラベルがない場合、break は直近の for 文しか終了させることができません。

フラグ変数を用意して各階層でチェックするよりも、ラベルを使用した方がシンプルで読みやすいコードになります。

効率的なループを書くためのベストプラクティス

Go言語で高パフォーマンスなプログラムを書くためには、ループ内でのリソース消費に注意を払う必要があります。

特にメモリ割り当てとスライスの扱いは、実行速度に直結します。

メモリ割り当ての最適化

ループの中でスライスに要素を追加( append )し続けると、内部的なメモリ再確保が頻繁に発生します。

あらかじめ必要な要素数がわかっている場合は、make 関数でキャパシティを指定しておくべきです。

Go
// 悪い例:ループごとに再確保が走る可能性がある
var data []int
for i := 0; i < 10000; i++ {
    data = append(data, i)
}

// 良い例:最初にメモリを確保するため高速
data := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    data = append(data, i)
}

事前にメモリを確保しておくことで、ガベージコレクション(GC)の負荷を軽減できます。

パフォーマンスを意識したrangeの選択

巨大な構造体( struct )の配列を range で回す場合、値のコピーが発生することに注意が必要です。

Goの range は、各要素をループ変数にコピーして渡します。

Go
type HeavyStruct struct {
    Data [1024]int
}

// 各要素がコピーされるため、メモリ転送コストが高い
for _, v := range largeSlice {
    _ = v.Data[0]
}

// インデックスのみ取得すればコピーは発生しない
for i := range largeSlice {
    _ = largeSlice[i].Data[0]
}

最新のGoコンパイラは非常に優秀であり、多くの場合で最適化が行われますが、極めて大きなデータを扱う際はインデックス経由のアクセスを検討してください。

まとめ

Go言語の for 文は、たった一つのキーワードでありながら、その記述スタイルによって多様な繰り返し処理をカバーしています。

基本となる3つのパーツからなるループ、while 文のように振る舞う条件付きループ、そしてコレクションを安全に扱う range 構文は、Goプログラミングの基礎中の基礎です。

さらに、Go 1.23で導入されたイテレータのサポートにより、独自のデータ構造に対しても標準的な for-range が利用可能になり、コードの抽象化レベルが一段階向上しました。

新しいイテレータの仕組みは、メモリ効率の向上や、よりクリーンなAPI設計に大きく寄与します。

今回紹介した基本ルールと最新のイテレータ仕様を正しく理解し、シンプルでパフォーマンスの高いGoコードを執筆していきましょう。

最新の言語仕様を積極的に取り入れることで、2026年以降のモダンな開発現場においても通用する、堅牢なシステムを構築できるはずです。