Tech

Go

Go

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

はじめに 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 生成と処理が交互に実行されています。これが遅延評価の特徴です。 実行フローの違い スライスの場合 createSliceが全要素を生成 スライスがメモリ上に確保される rangeが各要素を順次処理 ジェネレータの場合 rangeがiter.Seq型の関数を実行 yieldが呼ばれるたびにループ本体が実行される 次の要素が必要になるまで生成処理は進まない 注目すべきは、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時間と引き換えにメモリ効率の良い遅延評価 適切なパターンを選択することで、パフォーマンスとリソース使用量のバランスを最適化してきましょう。 参考資料 Go 1.23 Release Notes - Iterators

GoでEnumを表現する2つのパターン

Goには、Pythonなどの言語にあるような厳密なEnum型が存在しません。そのため、独自の型定義と定数を組み合わせてEnumを表現するのが一般的です。 今回は、実務でよく使われる2つの表現パターンについて紹介します。 【パターン1】 iota を活用した連番定義 iotaは、定数ブロック内で連番を自動的に割り振るための仕組みです。(Goに備わっている標準機能です) type ErrorCode int const ( BadRequest ErrorCode = iota + 1 // 1 Required // 2 InvalidChars // 3 InternalError // 4 ) iotaの初期値について Goの整数型のゼロ値は0です。未初期化の変数と明示的な値を区別するため、iota + 1として1から開始するのが一般的なプラクティスです。 定義したEnum値に対して文字列を返したい場合は、以下のように String() メソッドを定義します。 func (e ErrorCode) String() string { switch e { case BadRequest: return "BadRequest" case Required: return "Required" case InvalidChars: return "InvalidChars" case InternalError: return "InternalError" default: return fmt.Sprintf("ErrorCode(%d)", e) } } これにより、fmt.Printlnなどで出力する際に、数値ではなく意味のある文字列として扱えるようになります。 【パターン2】 mapベースで管理していく 定数で定義した値をキーとして、対応するメッセージをmapに格納する方法です。フォーマット可能な文字列や複雑なメタデータを関連づけたい場合に有効なパターンになります。 基本的な実装例 type ErrorCode int const ( BadRequest ErrorCode = iota + 1 Required InvalidChars InternalError ) var messageMap = map[ErrorCode]string{ BadRequest: "Bad Request error", Required: "%sを入力してください", // ... 続く } func (e ErrorCode) Message() string { if msg, ok := messageMap[e]; ok { return msg } return "Unknown Error Msg" } パラメータをつけた実装例 func (e ErrorCode) FormatMessage(args ...interface{}) string { template := e.Message() return fmt.Sprintf(template, args...) } msg := Required.FormatMessage("ユーザー名") // 出力: ユーザー名を入力してください ▶︎ こちらで試せます まとめ GoでEnum的な構造を実現する際は、要件に応じて使い分けましょう。 パターン1 シンプルで高速な文字列変換が必要な場合 パターン2動的なメッセージ生成や複雑なメタデータ管理が必要な場合 基本的にはパターン1をベースにし、複雑な関連データが必要になった段階でパターン2を検討するのが、Goらしいのかなと思っています。