Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Goイテレーターのエラー処理

Avatar for Lapi Lapi
October 12, 2026
6

 Goイテレーターのエラー処理

Avatar for Lapi

Lapi

October 12, 2026

Transcript

  1. 復習: Goのイテレーター Go 1.23 から、関数を for range で回せるようになった 関数の型 受け取り方

    型名( iter パッケージ) なし func(yield func() bool) for range f func(yield func(V) bool) for v := range f iter.Seq[V] func(yield func(K, V) bool) for k, v := range f iter.Seq2[K, V] が受け取る引数の数(0〜2個)が、 for range で受け取れる変数の数に対応する。 引数が1個・2個の関数型には、標準の iter パッケージで iter.Seq / iter.Seq2 という型名 が付いている。イテレーターを返す関数の戻り値には、ふつうこの型を使う。 yield 3
  2. 復習: Goのイテレーター yield に渡した値を、ループで受け取れる func Evens(n int) iter.Seq[int] { return

    func(yield func(int) bool) { for i := 0; i < n; i += 2 { if !yield(i) { return } } } } を呼ぶたびに、ループ本体が1回実 行される。 ループ側で break すると yield は false を 返す。イテレーター側はそれを見て return し、反復を終える。 yield(i) for v := range Evens(10) { fmt.Println(v) // 0, 2, 4, 6, 8 } 4
  3. 実用例: 部署ツリーの配下をすべてたどる 再帰でたどる木構造も、呼び出し側は for range だけで回せる type Department struct {

    ID string // 配下から目的の部署を探す for dept := range root.All() { Children []Department if dept.ID == targetID { } found = dept // All break // は配下の部署を深さ優先ですべて返す func (d Department) All() iter.Seq[Department] { return func(yield func(Department) bool) { for _, c := range d.Children { if !yield(c) { return } 見つかった時点で走査が止まる } } 使う側は探索方法などを気にせず、標準の range 記 法で書くことができる for g := range c.All() { if !yield(g) { return } } } } } 5
  4. イテレーター自体が失敗しうるとき、エラーはどう返す? ファイルや HTTP レスポンスも for range で1件ずつ処理したい。ただし、これらは途中で失 敗しうる ファイルを1行ずつ走査したい 大きなファイルを全部メモリに載せず、読んだそばか

    ら1行ずつ処理したい HTTP レスポンスをストリーム的に処理したい 全部届くのを待たずに、届いたそばから1件ずつ処理 したい エラーの返し方に公式の推奨パターンはまだなく、議論が続いている(golang/go#71901)。今 回の発表では以下の2つを紹介する。 1. パターンA: iter.Seq2[T, error] で値とエラーを一緒に返す 2. パターンB: Err() メソッドで、ループの後にエラーを返す 6
  5. パターンA: iter.Seq2[T, error] func Lines(path string) iter.Seq2[string, error] { for

    line, err := range Lines("app.log") { return func(yield func(string, error) bool) { if err != nil { f, err := os.Open(path) if err != nil { yield("", err) // defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { if !yield(sc.Text(), nil) { return } } if err := sc.Err(); err != nil { } } fmt.Println(line) } } } } 開始前のエラーも要素として流すしかない return yield("", err) // return err 「流したら即 return」は暗黙の規約 Pros で自然に書ける 要素ごとのエラーを扱える Cons ファイルのオープン失敗やクローズ失敗のような、前後 処理のエラーの扱いが難しい エラーが出たら反復を止めるのか続けるのか、といっ た規約を型で表現できない for v, err := range 7
  6. パターンB: Err() メソッド type LineReader struct { path string err

    lr := &LineReader{path: "app.log"} for line := range lr.Lines() { error fmt.Println(line) } } if err := lr.Err(); err != nil { func (r *LineReader) Lines() iter.Seq[string] { return func(yield func(string) bool) { f, err := os.Open(r.path) if err != nil { r.err = err return } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { if !yield(sc.Text()) { return } } r.err = sc.Err() } } func (r *LineReader) Err() error { return r.err } return err } Pros エラーはループの後に1回確認すればよい 反復の前後で起きるエラーも、「終わった理由」として 自然に返せる Cons Err() を呼び忘れても気づけない 要素ごとのエラーを表現できない 8
  7. そもそもイテレーターを使うべきではない? 「エラーが起きうる処理にはイテレーターを使うべきではない」という意見もある いまの状況 golang/go#71901 で「 iter.Seq などは失敗しな い反復に限るべき」という意見が出ている 標準ライブラリには、 iter.Seq2[T,

    error] を返 す API が1つもない os.File.ReadDir のイテレーター版 (golang/go#70084)も、エラーの返し方が決ま らず Open のまま そう言われる理由 全体のエラーも要素ごとのエラーも報告するよい方 法がない 途中終了時の Close も失敗しうるが、 for range は結果を返さないのでループの外で扱うしかない ログやトレースのための context.Context を、 for range が暗黙に呼ぶイテレーターに渡す場所 がない 9
  8. 参考資料 Go Blog: Range Over Function Types https://go.dev/blog/range-functions golang/go#61897 iter:

    new package for iterators https://github.com/golang/go/issues/61897 golang/go#71901 iter: document general guidance for writing iterator APIs https://github.com/golang/go/issues/71901 golang/go#70084 proposal: os: add iterator variant of File.ReadDir https://github.com/golang/go/issues/70084 Error handling with iterators in Go https://sinclairtarget.com/blog/2025/07/error-handling-with-iterators-in-go/ 12
  9. Appendix: AIエージェントの応答を画面に表示する LLMエージェントの応答を、ストリーム的に画面表示するプログラムをイテレーターを使って書 いてみた例 // エージェントの中で起きたこと1つ = イベント1つ for ev,

    err := range agent.Run(ctx, prompt) { type Event struct { if err != nil { テキスト応答 ToolCall *ToolCall // ツール呼び出し ToolResult *ToolResult // ツールの実行結果 Text string // // LLM 呼び出しの失敗など return err } } // 届いたイベントを、その場で画面に出す switch { はエージェントを実行し、 // 発生したイベントを順に返す // Run case ev.ToolCall != nil: ツール呼び出し] %s\n", ev.ToolCall.Name) case ev.ToolResult != nil: // ツールはエージェント内で実行済み fmt.Printf("[ツール結果] %s\n", ev.ToolResult.Output) fmt.Printf("[ func (a *Agent) Run(ctx context.Context, prompt string) iter.Seq2[*Event, error] default: fmt.Println(ev.Text) } } 13
  10. Appendix: AIエージェントの中身 ツールもイテレーターの内側で実行し、起きたことを yield する(簡略化した例) func (a *Agent) Run(ctx context.Context,

    prompt string) iter.Seq2[*Event, error] { return func(yield func(*Event, error) bool) { msgs := []Message{UserMessage(prompt)} for { resp, err := a.llm.Generate(ctx, msgs) // LLM を呼ぶ if err != nil { yield(nil, err) return } for _, ev := range resp.Events() { // テキストやツール呼び出しを返す if !yield(ev, nil) { return } } if resp.ToolCall == nil { // ツール呼び出しがなければ最終回答 return } ツールはイテレーターの内側で実行 if !yield(result.Event(), nil) { // 実行結果もイベントとして返す result := a.tools.Execute(ctx, resp.ToolCall) // return } msgs = append(msgs, resp.Message(), result.Message()) // 結果を LLM に渡す } } } 14