Tech

Goのイテレーター・ジェネレータについて

最終更新:2026.02.26

はじめに

Go 1.23でイテレータ機能が標準ライブラリに追加されました。
本記事では、新しく導入されたiterパッケージの使い方と、従来のスライスベースの反復処理との違いについて、実行フローとパフォーマンスについてまとめていきます。

イテレータとは

イテレータは、コレクションの要素を順次走査するための抽象化です。
Goでは従来からrangeを用いたスライスの反復処理が可能でしたが、Go1.23からは関数ベースのカスタムイテレータが言語レベルでサポートされるようになりました。

// 従来のスライスベースの反復処理
for i, v := range []string{"a", "b", "c"} {
    fmt.Println(i, v)
}

ジェネレータの概要

ジェネレータは、値を遅延評価的に生成するイテレータの一種です。Pythonのyieldキーワードのような専用構文はありませんが、iterパッケージで定義された型を使って実現します。

iter パッケージの型定義

package iter

type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
  • Seq[V]
    • 単一の値を返すイテレータ
  • Seq2[K, V]
    • キーと値のペアを返すイテレータ(mapのrangeループに相当)

yield関数の戻り値は継続フラグで、falseを返すとイテレーションが中断されます。

スライス vs ジェネレータ

スライスとジェネレータの実装について比較していきます。

【パターン1】 スライスベースの実装

func Test_Slice(t *testing.T) {
    strings := createSlice(5)
    for _, s := range strings {
        fmt.Printf("Test_Slice: %s\\n", s)
    }
}

func createSlice(max int) []string {
    slice := make([]string, 0, max)
    for i := range max {
        fmt.Printf("createSlice: %d\\n", i)
        slice = append(slice, strconv.Itoa(i))
    }
    return slice
}

実行結果:

createSlice: 0
createSlice: 1
createSlice: 2
createSlice: 3
createSlice: 4
Test_Slice: 0
Test_Slice: 1
Test_Slice: 2
Test_Slice: 3
Test_Slice: 4

スライス生成が完全に完了してから、rangeループによる反復処理が開始されます。

【パターン2】 ジェネレータベースの実装

func Test_Yield(t *testing.T) {
    stringGenerator := generateString(5)
    for s := range stringGenerator {
        fmt.Printf("Test_Yield: %s\\n", s)
    }
}

func generateString(max int) iter.Seq[string] {
    return func(yield func(string) bool) {
        for i := range max {
            fmt.Printf("generateString: %d\\n", i)
            if !yield(strconv.Itoa(i)) {
                return
            }
        }
    }
}

実行結果:

generateString: 0
Test_Yield: 0
generateString: 1
Test_Yield: 1
generateString: 2
Test_Yield: 2
generateString: 3
Test_Yield: 3
generateString: 4
Test_Yield: 4

生成と処理が交互に実行されています。これが遅延評価の特徴です。

実行フローの違い

スライスの場合

  1. createSliceが全要素を生成
  2. スライスがメモリ上に確保される
  3. rangeが各要素を順次処理

ジェネレータの場合

  1. rangeiter.Seq型の関数を実行
  2. yieldが呼ばれるたびにループ本体が実行される
  3. 次の要素が必要になるまで生成処理は進まない

注目すべきは、generateStringの戻り値である関数を明示的に呼び出していない点です。rangeキーワードがiter.Seq型を検出すると、自動的に関数を実行してイテレーションを開始します。

パフォーマンス特性の比較

項目スライスジェネレータ
メモリ使用量O(n) 全要素を保持O(1) 現在の状態のみ
初期化コスト高い – 全要素を事前生成低い – 遅延生成
反復処理速度高速 – メモリアクセスのみやや低速 – 毎回関数呼び出し
CPU使用率低い(反復時)高い(関数呼び出しオーバーヘッド)
早期終了時の効率無駄な生成が発生必要な分だけ生成

ベンチマーク例

func BenchmarkSlice(b *testing.B) {
    for i := 0; i < b.N; i++ {
        slice := createSlice(1000)
        for _, s := range slice {
            _ = s
        }
    }
}

func BenchmarkGenerator(b *testing.B) {
    for i := 0; i < b.N; i++ {
        gen := generateString(1000)
        for s := range gen {
            _ = s
        }
    }
}

使い分けの指針

スライスを選ぶべきケース

  • 全要素を複数回走査する必要がある
  • データサイズが小さく、メモリに余裕がある
  • 反復処理のパフォーマンスが重要
  • データを一度に取得するコストが低い

ジェネレータを選ぶべきケース

  • データサイズが大きく、メモリ効率が重要
  • 要素生成のコストが高い(DB クエリ、API コールなど)
  • 早期終了の可能性が高い(条件に合う最初の要素を探すなど)
  • 無限シーケンスを扱う場合

実践例:無限シーケンス

func infiniteCounter() iter.Seq[int] {
    return func(yield func(int) bool) {
        i := 0
        for {
            if !yield(i) {
                return
            }
            i++
        }
    }
}

// 最初の10個だけ取得
func Test_InfiniteCounter(t *testing.T) {
    count := 0
    for n := range infiniteCounter() {
        fmt.Println(n)
        count++
        if count >= 10 {
            break
        }
    }
}

このようなパターンはスライスでは実現できません。

まとめ

Go1.23のイテレータ機能は、従来のスライスベースの反復処理に加えて、メモリ効率の良い遅延評価を実現できます。

  • スライス
    メモリと引き換えに高速な反復処理
  • ジェネレータ
    CPU時間と引き換えにメモリ効率の良い遅延評価

適切なパターンを選択することで、パフォーマンスとリソース使用量のバランスを最適化してきましょう。

参考資料