【C#入門】【csharp】foreach文を完全マスター!基本からfor文との違い、注意点まで解説

C#入門】foreach文を完全マスター!基本からfor文との違い、注意点まで解説

C#でプログラミングをする上で、避けて通れないのが「繰り返し処理」です。その中でも、配列やリストを扱う際に最も頻繁に使われるのが foreachです。

今回は、C#foreach の基本的な使い方から、for 文との使い分け、そしてハマりやすい注意点までを徹底解説します。


1. foreach文とは?

foreach 文は、配列や List などのコレクション(データの集まり)の要素を、最初から最後まで順番に取り出して処理するための構文です。

「インデックス(要素番号)」を気にする必要がないため、コードがスッキリして読みやすくなるのが最大の特徴です。

基本構文

foreach (型 変数名 in コレクション)
{
    // ここで変数名を使って処理を行う
}

最近のC#では、型の部分に var型推論)を使うのが一般的です。


2. 基本的な使い方

では、実際にコードを見てみましょう。

リスト(List)の中身を表示する

例えば、果物の名前が入ったリストを順番に表示する場合です。

var fruits = new List<string> { "Apple", "Banana", "Orange" };

foreach (var fruit in fruits)
{
    Console.WriteLine(fruit);
}

実行結果:

Apple
Banana
Orange

このように、fruits リストから一つずつ要素を取り出し、変数 fruit に入れて処理してくれます。最後の要素まで行くと自動的にループが終了します。

辞書(Dictionary)をループする

Dictionary の場合、キー(Key)と値(Value)のペアを取り出すことができます。

var scores = new Dictionary<string, int>
{
    { "Tanaka", 80 },
    { "Suzuki", 95 },
    { "Sato", 70 }
};

foreach (KeyValuePair<string, int> entry in scores)
{
    // entry.Key で名前、entry.Value で点数を取得
    Console.WriteLine($"{entry.Key}: {entry.Value}点");
}

ポイント: KeyValuePair<...> と書くのが長い場合は、ここでも foreach (var entry in scores) と書くことができます。


3. for文とforeach文の違い・使い分け

for 文と何が違うの?」と疑問に思うかもしれません。それぞれの特徴を比較してみましょう。

特徴 foreach文 for文
読みやすさ ◎ 非常にシンプル △ インデックス管理が必要
インデックス 使えない(何番目かわからない) 使える(i で管理可能)
要素の変更 不可(読み取り専用) 可能
逆順・飛び級 苦手 得意(i--i+=2 が可能)

どちらを使うべき?

  • 基本は foreach を使う: 配列の「全要素」を「順番」に見たいだけなら、foreach の方がバグ(範囲外アクセスなど)が起きにくく安全です。
  • for を使う時: 「3番目の要素だけ書き換えたい」「逆順に処理したい」「インデックス番号自体が必要」な場合は for を使います。

4. 【重要】foreachの注意点(やってはいけないこと)

foreach を使う上で、絶対に知っておくべきルールがあります。それは、ループ中にコレクションの中身を変更(追加・削除)してはいけないということです。

NG例:ループ中に削除しようとする

var numbers = new List<int> { 1, 2, 3, 4, 5 };

foreach (var n in numbers)
{
    if (n % 2 == 1)
    {
        // エラー発生!ループ中にコレクションを変更してはいけません
        numbers.Remove(n); 
    }
}

これを実行すると、InvalidOperationException というエラーが発生し、プログラムが止まります。

解決策

要素を削除したい場合は、以下のいずれかの方法を使います。

  1. for 文を逆順で回す
  2. RemoveAll メソッドを使う(推奨)
  3. 別のリストにコピーしてから foreach する (numbers.ToList() を使う)

5. まとめ

  • foreach はコレクションを順番に処理するのに最適
  • インデックス管理が不要でコードが読みやすい
  • Dictionary も簡単に回せる
  • ループ中に要素の追加・削除はできないので注意

C#を書く上で、foreach は最も頻繁に使う構文の一つです。まずは「全要素を処理したいときは foreach」と覚えておけば間違いありません!


【Go言語】`implements` 宣言は不要?「ダックタイピング」なインターフェースの衝撃

【Go言語】implements 宣言は不要?「ダックタイピング」なインターフェースの衝撃

JavaC#でインターフェースを使う時、まずは「契約(Contract)」としてインターフェースを定義し、それをクラスで「実装します!」と明示的に宣言(implements / :)しますよね。

しかし、Go言語には implements キーワードが存在しません。 では、どうやって「このクラスはこのインターフェースを満たしている」と判断するのでしょうか?

1. ダックタイピング:「アヒルみたいに歩くなら、それはアヒルだ」

Goのアプローチは 「構造的部分型(Structural Subtyping)」 、通称ダックタイピングと呼ばれるものです。

「もしそれがアヒルのように歩き、アヒルのように鳴くなら、それはアヒルである」

つまり、「あるインターフェースが要求するメソッドをすべて持っていれば、明示的な宣言がなくても、自動的にそのインターフェースを実装したとみなす」 というルールです。


2. コードで比較:Java/C# vs Go

「動物(Animal)」インターフェースと、それを実装する「犬(Dog)」クラスで比較してみましょう。

Java / C# の場合(明示的)

「私は Animal です!」と名乗る必要があります。

// C#
public interface IAnimal {
    void Speak();
}

public class Dog : IAnimal // ← ここで明示的に宣言が必要!
{
    public void Speak() {
        Console.WriteLine("ワン!");
    }
}

Go の場合(暗黙的)

Dog 構造体は Animal インターフェースのことなど知らなくても構いません。ただメソッドを持っていればいいのです。

// Go
// 1. インターフェース定義
type Animal interface {
    Speak()
}

// 2. 構造体定義(Animalについては何も言及しない)
type Dog struct {}

// 3. メソッド実装(これで自動的に Animal を満たしたことになる)
func (d Dog) Speak() {
    fmt.Println("ワン!")
}

// 使い方
func main() {
    var a Animal
    a = Dog{} // コンパイルOK! DogはSpeak()を持ってるから。
    a.Speak()
}

もし Dog から Speak() メソッドを削除したり、引数を変えたりすると、a = Dog{} の代入箇所でコンパイルエラーになります。「DogAnimal インターフェースを満たしていません」と怒られます。


3. なぜこの仕様なのか?(Java/C#エンジニアへのメリット)

「明示しないと分かりにくくない?」と不安になるかもしれません。しかし、この設計には強力なメリットがあります。それは「利用する側(Consumer)がインターフェースを定義できる」という点です。

Java/C# の悩み

外部ライブラリのクラス SomeLibClass をテストでモックしたい時、そのクラスが ISomeLibClass を実装していなければ、モックを作るのは大変です(ラッパークラスを作ったりする必要があります)。

Go の解決策

外部ライブラリがインターフェースを提供していなくても構いません。あなたが自分のコードの中で、「このメソッドさえあればいい」というインターフェースを勝手に作ればいいのです。

// 外部ライブラリ(変更できない)
type ExternalLogger struct { ... }
func (l ExternalLogger) Log(msg string) { ... }

// 自分のコード
// ExternalLoggerに合わせて、勝手にインターフェースを作る
type MyLogger interface {
    Log(msg string)
}

// この関数は、ExternalLoggerでも、自作のMockLoggerでも受け取れる!
func DoSomething(logger MyLogger) {
    logger.Log("Hello")
}

これにより、依存関係の結合度(Coupling) が劇的に下がります。


4. interface{}any :究極の抽象型

Goには java.lang.ObjectC#object 型に相当する、「あらゆる型が入る型」があります。 それが interface{}(空のインターフェース) です。

メソッドの要件が何もない(空っぽ)ということは、「どんな型でもこの要件(虚無)を満たしている」 と言えるからです。

Go 1.18 からは、この interface{}エイリアスとして any というキーワードが導入されました。Java/C#エンジニアにはこちらの方が馴染みやすいでしょう。

func PrintAnything(v any) {
    fmt.Println(v)
}

PrintAnything(100)      // OK
PrintAnything("hello")  // OK
PrintAnything(Dog{})    // OK

5. ダウンキャスト(型アサーション

any 型に入れた値を、元の型に戻したい時はどうするでしょうか? Java(String)objC#obj as string に相当するのが アサーション です。

var i any = "hello"

// Java: String s = (String) i;
s := i.(string) // 成功すれば s に "hello" が入る

// 安全なキャスト(C#の as に近い)
// s, ok := i.(string)
s, ok := i.(string)
if ok {
    fmt.Println("文字列です:", s)
} else {
    fmt.Println("文字列ではありません")
}

ここでも、チャネルの時に出てきた「Comma-ok イディオム」が登場しますね!

taglibrary.hatenablog.com


まとめ

  1. 宣言不要: メソッドシグネチャが一致すれば、自動的に実装したとみなされる。
  2. 後出しジャンケンOK: クラス(構造体)を作った後に、それに合わせたインターフェースを定義しても機能する。
  3. any (interface{}): JavaObject のように何でも入る型だが、使う時は型アサーションが必要。

JavaC#では「設計段階でインターフェースをきっちり決める」ことが重要でしたが、Goでは「必要になったらインターフェースを切り出す」という、よりプラグマティック(実用的)なスタイルが好まれます。

【Go言語】Java/C#エンジニアが「finally」から解放される魔法の言葉 `defer`とは

この記事では筆者のようなJavaC#をメインで利用している方向けにGoの書き方を説明しています。 書きっぷりがGoはjava/C#よりすごいぜって感じになってますがご容赦ください。

【Go言語】Java/C#エンジニアが「finally」から解放される魔法の言葉 defer

JavaC#でリソース管理(ファイルの読み書き、DB接続、ロックの取得など)を行う際、コードがネスト地獄になったり、try-catch-finally が長くなりすぎて「本来の処理」がどこにあるか見失ったりした経験はありませんか?

Go言語の defer は、そんな悩みを解決する強力なキーワードです。今回はこの defer を、Javatry-finallyC#using ステートメントと比較しながら解説します。


1. 一言で言うと:「予約された後始末」

defer は、「この関数(メソッド)が終了する直前に、必ずこの処理を実行してね」 と予約する命令です。

JavaC#の感覚で言うと、「関数全体を包む巨大な try-finally ブロックを、たった1行で宣言する」 ようなものです。


2. Java/C# との比較:ファイルの読み込み

「ファイルを開いて、何か書き込んで、最後に必ず閉じる」という処理を比べてみましょう。

Java (Classic Style) / C# (Old Style)

リソース解放のために finally ブロックが必須です。変数のスコープを気にする必要もあり、記述が冗長になりがちです。

// Java
FileInputStream fis = null;
try {
    fis = new FileInputStream("test.txt");
    // ... 処理 ...
} catch (IOException e) {
    // ... エラー処理 ...
} finally {
    if (fis != null) {
        try { fis.close(); } catch (IOException e) {}
    }
}

C# (using statement) / Java (try-with-resources)

これらは非常に優秀な構文ですが、「ネスト(インデント)」が深くなるという欠点があります。


// C#
using (var reader = new StreamReader("test.txt")) 
{
    // ... ここで処理 ...
    // インデントが1段深くなる
} // ここで自動的に Dispose() される

Go (defer)

Goには try-finallyusing もありません。代わりにこう書きます。

func processFile() error {
    f, err := os.Open("test.txt")
    if err != nil {
        return err
    }
    // 【ここがポイント!】
    // 開いた直後に「関数終了時に閉じること」を予約
    defer f.Close() 

    // ... 処理 ...
    // ... 処理 ...

    return nil 
    // ここで自動的に f.Close() が実行される
}

Java/C#エンジニアが感じるメリット:

  1. ネストが深くならない: using の波括弧 {} が不要です。
  2. 宣言と解放がセット: Open した直後に Close を書けるため、「閉じ忘れ」を目視で確認しやすくなります。処理が何百行続いても、リソース管理のコードが離れ離れになりません。

3. defer の重要な挙動:LIFO(後入れ先出し)

defer を複数書いた場合、どのような順序で実行されるのでしょうか? ここがスタック(Stack)構造になっています。

func main() {
    defer fmt.Println("1番目: データベース切断")
    defer fmt.Println("2番目: ファイルクローズ")
    defer fmt.Println("3番目: 計算ログ出力")

    fmt.Println("メインの処理実行中...")
}

出力結果:

メインの処理実行中...
3番目: 計算ログ出力
2番目: ファイルクローズ
1番目: データベース切断

Java/C#で、ネストした using ブロックや try-finally が内側から順に解放されていくのと同じ順序(逆順)です。 「最後に開いたものを最初に閉じる」というリソース管理の鉄則が、構文レベルで保証されています。


4. よくある落とし穴:引数の評価タイミング

Java/C#エンジニアが最も引っかかりやすいのがここです。 defer に渡した関数の引数は、defer を宣言した瞬間に評価(固定)される」 というルールがあります。

クイズ

以下のコードは最後に何を出力するでしょうか?

func demo() {
    i := 10
    defer fmt.Println("deferの値:", i) // ここで宣言
    
    i = 20 // 後で値を変更
    fmt.Println("現在の値:", i)
}

答え

クリックで表示

現在の値: 20
deferの値: 10

「あれ、最後は20じゃないの?」と思うかもしれません。 しかしGoは、defer fmt.Println(..., i) を通過した瞬間の i の値(つまり10)をコピーして保存します。そのため、後で i がどう変わろうと、終了時には 10 が出力されます。

回避策(ポインタやクロージャを使う): もし「終了時の最新の値」を使いたい場合は、無名関数(クロージャ)を使います。

defer func() {
    // 関数実行時に i を参照しにいく
    fmt.Println("deferの値:", i) 
}()

これなら 20 が出力されます。C#ラムダ式における変数のキャプチャに近い挙動ですね。


まとめ

  • Goの defer は、Javatry-finallyC#using の代わりになる、よりフラットで可読性の高いリソース管理機構です。
  • 「開いたらすぐ defer Close() という手癖をつけるだけで、リソースリークを劇的に減らせます。
  • 実行順序はLIFO(逆順) なので、依存関係のあるリソース解放も安全です。

【Go言語】【カンマ OK イディオム】Java/C#エンジニアが最初に戸惑う `case msg, ok := <-ch`(キューの監視) の正体を解剖する

【Go言語】Java/C#エンジニアが最初に戸惑う case msg, ok := <-ch の正体を解剖する

JavaC#で長年バックエンド開発をしてきた方がGo言語のコード(特にWebSocketやチャットサーバー、ワーカープールなどの実装)を読み始めると、必ずと言っていいほどこの構文に出くわします。 筆者はJavaC#をメインで使ってきたのでこの文法に戸惑いました。

select {
case message, ok := <-c.send:
    if !ok {
        // ... エラー処理やクローズ処理 ...
    }
    // ... 正常系処理 ...
}

caseswitch 文みたいなものだろう」というのは想像がつきますが、<- という矢印や、変数が2つ(message, ok)返ってきている部分など、Java/C#の感覚とは少し異なる挙動をしています。

この記事では、この「Go特有のイディオム」を、Java/C#の概念と比較しながら噛み砕いて解説します。


1. 全体像:これは「ノンブロッキングなキューの監視」

まず、JavaC#の概念に置き換えてみましょう。

  • Goの c.send (チャネル): JavaBlockingQueueC#BlockingCollection のような、スレッドセーフなキューだと考えてください。
  • Goの select: 複数のキューを同時に監視し、「最初にデータが届いたもの」を処理するための構文です。
  • <- (受信演算子): キューからデータを取り出す(Dequeue)操作です。

つまり、このコード全体は c.send というキューからデータを取り出そうとしている。もし取り出せたらそのデータを処理する」 ということを行っています。


2. 構文の徹底分解

では、case message, ok := <-c.send: をパーツごとに分解します。

<-c.send (受信)

これは「チャネル c.send から値を受信する」という命令です。 Javaで言えば queue.take() に相当します。データが来るまで待機(ブロック)しますが、select の中にある場合は「データが来たらここを実行する」というトリガーになります。

message (値の受け取り)

チャネルから送られてきたデータそのものがここに入ります。 string 型のチャネルなら文字列が、struct のチャネルなら構造体が入ります。

ok (最重要:生存確認フラグ)

ここがJava/C#開発者にとっての肝です。 Goのチャネルには「オープン(開いている)」と「クローズ(閉じている)」という状態があります。

  • oktrue: チャネルは開いており、正常にデータを受信しました。
  • okfalse: チャネルは既に close() されており、これ以上データは送られてきません。

:= (短縮変数宣言)

右辺の結果を使って、左辺の messageok という変数をその場で定義して代入しています。Javavar に近い感覚です。


3. なぜ ok が必要なのか?(Java/C#との違い)

JavaC#BlockingQueue からデータを取り出す際、もしキューが閉鎖されていたり、読み込み中に割り込みが入ったりすると、通常は 例外(Exception) が発生するか、null が返ることが多いでしょう。

しかし、Goの哲学では例外(パニック)を極力避けます。 Goのチャネルは、クローズされた後でも読み込みが可能です。 クローズされたチャネルから読み込むと、エラーにはならず、その型の「ゼロ値(空文字や0、nilなど)」が即座に返されます。

これでは、「本当に空文字が送られてきた」のか「チャネルが閉じたから空文字が返ってきた」のか区別がつきません。

Mapの例え: C#Dictionary.TryGetValue(key, out var value)Javaのマップ操作に似ています。「値」と「成功したかどうかのブール値」をセットで返す。これをGoでは「Comma-ok イディオム」と呼びます。

Java/C#脳 vs Go脳

特徴 Java (BlockingQueue) / C# (BlockingCollection) Go (Channel)
データ取得 queue.take() <-ch
終了検知 例外 (InterruptedException) や null チェック、または IsCompleted プロパティ 2つ目の戻り値 ok をチェック
クローズ後の挙動 例外が飛ぶか、読み込めなくなる ゼロ値が無限に返り続ける

この「ゼロ値が無限に返り続ける」挙動を防ぐために、if !ok でチェックし、チャネルが閉じていたらループを抜けるなどの処理が必須になるのです。


4. 具体的なコード比較

イメージを掴むために、C#とGoで似たような処理を書いてみます。

C# の場合 (BlockingCollection)



foreach (var message in collection.GetConsumingEnumerable())
{
    // データがある間はループする
    Process(message);
}
// コレクションがCompleteAdding()されると自動でループを抜ける

Go の場合 (Comma-ok パターン)

for {
    select {
    // c.sendからデータを受信、同時にチャネルの状態(ok)も取得
    case message, ok := <-c.send:
        if !ok {
            // チャネルが閉じているのでループを抜ける(必須!)
            return
        }
        // 正常なデータ処理
        Process(message)
    }
}

まとめ

case message, ok := <-c.send: という一行は、以下の3つのことを同時に行っています。

  1. 待機: c.send にデータが来るのを待つ。
  2. 受信: データ (message) を受け取る。
  3. 生存確認: チャネルがまだ有効か (ok) を確認する。

JavaC#から来ると、最初は「戻り値が2つある」「例外を使わない」という点に違和感があるかもしれません。しかし、これは「並行処理における状態変化を、値として安全に扱う」というGo言語らしいスマートな設計なのです。

Google発のAIエディタ「Antigravity」が登場!CursorやWindsurfとの違いと使い方を徹底解説

エンジニアは「書く」から「指揮する」へ。Google Antigravityが切り拓くエージェント型開発の未来

本文構成

はじめに

2025年11月18日、GoogleがGemini 3と同時に発表した衝撃的なツール、それがGoogle Antigravity」です。 これまでのAIエディタ(CursorやVS Code Copilotなど)は、あくまで「人間のコーディングを補助する副操縦士(Co-pilot)」でした。しかし、Antigravityはその概念を覆し、「AIエージェントが自律的に開発を行い、人間がそれを管理する」というAgent-first(エージェントファースト)な体験を提供します。

本記事では、この革新的なツールの概要と、具体的な使い方について解説します。


1. Google Antigravityとは?

Google Antigravityは、Googleが提供する新しい統合開発環境IDE)プラットフォームです。 Visual Studio CodeVS Code)をベースに作られていますが、最大の特徴は「自律型AIエージェント」IDEの核心に据えている点です。

既存ツールとの決定的な違い

特徴 従来のAIエディタ (Cursor等) Google Antigravity
役割 コードの補完、チャットでの質問回答 タスクの計画・実行・検証の自律遂行
操作範囲 主にエディタ内のテキスト エディタ、ターミナル、ブラウザを横断操作
ユーザーの立ち位置 ドライバー(実際に手を動かす) マネージャー(指示を出し承認する)
並列処理 基本的に1つのチャットで対話 複数のエージェントを同時に稼働可能

Antigravityでは、ユーザーはコードを一行一行書くのではなく、「エージェントにミッションを与え、その成果物(Artifacts)をレビューする」という働き方にシフトします。


2. Antigravityの凄さを支える3つの機能

① Agent Manager(エージェントマネージャー)

これがAntigravityの司令塔です。ユーザーはここで「認証機能を追加して」「UIをモダンにして」といったタスクを投げます。すると、AIエージェントが自律的に以下のサイクルを回します。

  1. Plan(計画): 何をすべきかタスクを分解する。
  2. Act(実行): コードを書き、ターミナルでコマンドを叩き、ライブラリをインストールする。
  3. Verify(検証): 内蔵ブラウザを立ち上げて動作確認を行う。

② Artifacts(成果物)による透明性

AIに任せきりにするのは不安ですが、Antigravityは「Artifacts」という形で、AIの思考プロセスや成果を可視化します。

これらを確認できるため、ブラックボックス化せず、安心してマージできます。

③ マルチモデル対応とGemini 3 Pro

Google純正ツールですが、搭載モデルは柔軟です。

  • Gemini 3 Pro: Googleの最新・最強モデル。推論能力が飛躍的に向上。
  • Claude Sonnet: Anthropic社のモデルも選択可能。
  • OpenAI GPT-OSS: オープンソース版モデルもサポート。

3. Google Antigravityの使い方(基本フロー)

現在はプレビュー版として公開されており、以下の手順で利用できます。

ステップ1:インストールとセットアップ

  1. 公式サイト(antigravity.google)からインストーラーをダウンロード(Mac/Windows/Linux対応)。
  2. アプリを起動し、Googleアカウントでログインします。
  3. VS Codeベースなので、既存のVS Code設定や拡張機能をインポートすることも可能です。

ステップ2:プロジェクトの開始

  • Editor View: 通常のVS Codeのようにコードを書く画面。
  • Manager View: エージェントを管理する画面。

まずは「Manager View」を開き、新しいプロジェクトを作成するか、既存のフォルダを開きます。

ステップ3:エージェントへの指示出し(タスク委譲)

チャット欄に自然言語で指示を出します。

プロンプト例: 「Next.jsでToDoアプリを作成してください。Tailwind CSSを使ってモダンなデザインにし、タスクの永続化機能も付けて。」

ステップ4:自律動作の観察とレビュー

エージェントが動き出します。

  • 自動でファイルが作成され、コードが書かれます。
  • ターミナルが開き、npm installnpm run dev が実行されます。
  • ここが凄い!:エージェントはブラウザを操作して、実際にアプリが動いているかを確認し、エラーがあれば自己修正します。

ユーザーは画面右側のパネルで、エージェントが作成した「計画」や「スクリーンショット」を確認し、問題なければ「Approve(承認)」や修正指示を出します。

ステップ5:マルチエージェントでの並列作業

ここがAntigravityの真骨頂です。

  • エージェントAに「フロントエンドのボタン修正」を依頼。
  • その間に、エージェントBに「バックエンドのAPIテスト作成」を依頼。

このように、複数のタスクを同時に別々のエージェントに投げ、自分はそれぞれの進捗を管理することができます。


4. まとめ:エンジニアの仕事はどう変わる?

Google Antigravityは、単なる便利ツールではなく、開発プロセスの再定義」です。

  • コーディング時間の短縮: 定型的なコードはAIが瞬殺。
  • 設計への集中: 人間は「どう作るか」より「何を作るか(仕様・体験)」に集中できる。
  • 1人でチーム開発: 複数のエージェントを使役することで、1人のエンジニアがチーム並みの生産性を出せる可能性。

現在はプレビュー期間中で、Gemini 3 Proなどの高性能モデルも(制限付きですが)無料で試せるようです。 「AIにコードを書かせる」から「AIと一緒に開発チームを作る」体験へ。ぜひ一度、Google Antigravityを触ってみてください。


参考リンク:


【Go言語】【完結編】`errgroup`:並行処理の「めんどくさい」を全部やってくれる魔法のツール

【完結編】errgroup:並行処理の「めんどくさい」を全部やってくれる魔法のツール

これまで、sync.WaitGroup を使い、チャネルでエラーを運び、select で停止信号を監視する……という実装をしてきました。勉強には最適ですが、本音を言えば 「もっと楽に書きたい」 ですよね?

そこで登場するのが、Go公式の準標準ライブラリ golang.org/x/sync/errgroup です。

これを使うと、前回の記事で書いた何十行ものコードが、驚くほど短くなります。

なぜ errgroup なのか?

errgroup は、以下の「並行処理あるある」を自動化してくれます。

  1. WaitGroupの管理不要: AddDone を自分で書かなくていい。
  2. エラーの自動伝播: 誰か1人がエラーを出したら、それをメインに報告してくれる。
  3. Contextの連動: 誰か1人が失敗したら、他の全員に自動でキャンセル信号(Context)を送る
  4. 並行数の制限: ワーカープールを自作しなくても、メソッド1つで同時実行数を制限できる。

準備

標準ライブラリではないので、インストールが必要です。

go get golang.org/x/sync/errgroup

実装コード:驚きの短さ

前回の「3つのワーカーで処理し、エラーがあったら止まる」という要件を errgroup で書き直してみます。

package main

import (
    "context"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func process(id int) error {
    fmt.Printf("Job %d 開始\n", id)
    time.Sleep(500 * time.Millisecond) // 処理のフリ

    // 偶数は失敗させるシミュレーション
    if id%2 == 0 {
        return fmt.Errorf("Job %d でエラー発生!", id)
    }

    fmt.Printf("Job %d 完了\n", id)
    return nil
}

func main() {
    // コンテキスト付きのerrgroupを作成
    // g: グループ管理オブジェクト
    // ctx: 誰かが失敗した瞬間にキャンセルされるコンテキスト
    g, ctx := errgroup.WithContext(context.Background())

    // ★ここが最強機能:同時実行数を「3」に制限(ワーカープールの代替)
    g.SetLimit(3)

    jobList := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    for _, jobID := range jobList {
        // 変数のキャプチャ(Go 1.22以降なら不要だが念のため)
        id := jobID

        // g.Go でゴルーチンを起動
        // SetLimitしているので、3つ以上は同時に動かず待機してくれる
        g.Go(func() error {
            // Contextのキャンセルチェック
            // (他の誰かがエラーを出したら ctx.Done() が閉じる)
            select {
            case <-ctx.Done():
                return ctx.Err()
            default:
                // 通常処理
                return process(id)
            }
        })
    }

    // 全部の処理が終わるか、誰かがエラーを出すまで待つ
    if err := g.Wait(); err != nil {
        fmt.Printf("\n【失敗】エラーが発生しました: %v\n", err)
    } else {
        fmt.Println("\n【成功】すべての処理が完了しました")
    }
}

コードの比較:何が良くなった?

1. ワーカープールを作る必要がない

g.SetLimit(3) と書くだけで、内部的にセマフォを使った同時実行制御が行われます。わざわざ for select でワーカー関数を作る必要がなくなりました。

2. エラー処理が単純

return err するだけで、g.Wait() がそのエラーを受け取ってくれます。Result構造体を作ってチャネルに流す手間が消えました。

3. キャンセル処理が自動

errgroup.WithContext を使っているため、ゴルーチンの1つが return err すると、自動的に ctx がキャンセルされます。他のゴルーチンは ctx.Done() を検知して即座に撤退できます。

まとめ:使い分けの基準

これまでの知識をどう使い分けるべきでしょうか?

パターン おすすめの場面
for select 最も基本。無限ループで常駐する処理や、複雑なチャネル制御が必要な場合。
Worker Pool 基本の勉強、あるいは errgroup では制御しきれない細かい挙動(例:エラーでも止めずに集計したい等)が必要な場合。
errgroup 実務の9割はこれ。 API並列リクエスト、バッチ処理など、「まとめてやって、結果を知りたい」場合。

これで、Goの並行処理における「基礎」から「実務レベルの最適解」までをマスターしました。 for select の仕組みを知っているからこそ、errgroup のありがたみが分かるはずです。

ぜひ、次回のプロジェクトでは errgroup を導入して、コードを劇的に短くしてみてください!

【Go言語】 【発展編】ワーカープールに「エラーハンドリング」を実装する

【発展編】ワーカープールに「エラーハンドリング」を実装する

前回の記事 では、並行数を制御するワーカープールを作成しました。しかし、実務の現場では「処理が失敗したとき、どうするか?」を必ず考慮しなければなりません。

単に panic させてしまうと、ワーカー(ゴルーチン)自体が死んでしまい、プールが縮小してしまいます。

今回は、「ワーカーを止めずにエラーだけを報告する」堅牢な設計パターンを紹介します。

戦略:Result構造体パターン

チャネルで単純な値(intなど)だけを返すと、エラーが発生したかどうかが分かりません。 そこで、結果(Value)とエラー(Error)をセットにした構造体を定義して、それをチャネルで返します。

Go言語の関数が (int, error) を返すのと似た感覚を、チャネル経由で実現します。

実装コード:エラーを報告するワーカー

偶数のジョブIDが「失敗する」ようにシミュレートしたコードです。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// Result: 処理結果とエラーをひとまとめにする構造体
type Result struct {
    JobID int
    Value int
    Err   error
}

// 処理をシミュレートする関数(たまにエラーになる)
func process(jobID int) (int, error) {
    time.Sleep(500 * time.Millisecond) // 重い処理のフリ

    // ランダムに失敗させる(例:偶数はエラー)
    if jobID%2 == 0 {
        return 0, fmt.Errorf("データベース接続エラー")
    }
    return jobID * 10, nil
}

func worker(id int, jobs <-chan int, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()

    for jobID := range jobs {
        // 処理を実行
        val, err := process(jobID)

        // 成功・失敗にかかわらず、結果構造体を作って送信する
        // これによりワーカーは止まらず、次のジョブへ進める
        results <- Result{
            JobID: jobID,
            Value: val,
            Err:   err,
        }
    }
}

func main() {
    const numWorkers = 3
    const numJobs = 10

    jobs := make(chan int, numJobs)
    results := make(chan Result, numJobs) // Result構造体のチャネル
    var wg sync.WaitGroup

    // 1. ワーカー起動
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // 2. ジョブ投入
    go func() {
        for j := 1; j <= numJobs; j++ {
            jobs <- j
        }
        close(jobs)
    }()

    // 3. 結果収集の終了監視
    go func() {
        wg.Wait()
        close(results)
    }()

    // 4. 結果とエラーのハンドリング(ここが重要!)
    successCount := 0
    failCount := 0

    for res := range results {
        if res.Err != nil {
            // エラーの場合の処理
            fmt.Printf("[X] Job %d 失敗: %v\n", res.JobID, res.Err)
            failCount++
        } else {
            // 成功の場合の処理
            fmt.Printf("[O] Job %d 成功: 結果=%d\n", res.JobID, res.Value)
            successCount++
        }
    }

    fmt.Println("---")
    fmt.Printf("完了: 成功 %d件 / 失敗 %d件\n", successCount, failCount)
}

解説:このパターンのメリット

1. ワーカーの責務を分離する

ワーカー(worker関数)の中で「エラーログを出力」したり「プログラムを終了」させたりするのは悪手です。ワーカーはあくまで「やってみた結果(成功か失敗か)」を返すだけに徹します。

2. メイン側でポリシーを決められる

エラーを受け取った main 関数側で、どうするかを決定できます。

  • ログだけ出して無視する?
  • 失敗カウントが一定数を超えたら全体を止める?
  • 失敗したジョブを別のリスト(Dead Letter Queue)に入れる?

このように、「実行」と「判断」を分離できるのが疎結合な設計の基本です。


補足:リトライ(再試行)はどうする?

「ネットワークエラーなら1回だけやり直したい」という場合もよくあります。 もっともシンプルな方法は、ワーカー関数内で完結させることです。

func processWithRetry(jobID int) (int, error) {
    const maxRetries = 3
    for i := 0; i < maxRetries; i++ {
        val, err := process(jobID)
        if err == nil {
            return val, nil // 成功したら即return
        }
        fmt.Printf("Job %d リトライ中 (%d/%d)...\n", jobID, i+1, maxRetries)
        time.Sleep(1 * time.Second) // 少し待つ(重要)
    }
    return 0, fmt.Errorf("リトライ上限到達")
}

このように、小さなループを関数内に隠蔽することで、呼び出し元のチャネル構造を複雑にすることなくリトライ機能を実装できます。


まとめ

  • 並行処理の結果は、ValueError を含む構造体(Struct)で返す。
  • ワーカー内でエラーハンドリングを完結させず、報告としてメインルーチンに戻す。
  • これにより、システム全体をクラッシュさせることなく、部分的な障害を適切に処理できる。

ここまで実装できれば、本番環境でも十分に通用する堅牢なバッチ処理システムになります。