Go言語の並行処理パターン:goroutineとchannelによる非同期処理の最適解

当ページのリンクには広告が含まれています。

お疲れ様です!IT業界で働くアライグマです!

「Goを書いているけど、channelの使いどころがいまいち分からない…」
「goroutineを立ち上げすぎて、逆に遅くなったりパニックしたりする…」

こんな悩みを持つバックエンドエンジニアの方は多いのではないでしょうか?
Go言語の最大の特徴である並行処理(Concurrency)は強力ですが、正しく設計しないと「競合状態(Race Condition)」「デッドロック」の温床になります。

そこで今回は、現場でよく使われる「パイプライン」「ファンアウト/ファンイン」「ワーカープール」といった並行処理パターンを、具体的なコード例とともに解説します。これらを使いこなせれば、安全かつ高速な非同期処理を実装できるようになりますよ!

目次

並行処理の難しさと「Goの流儀」

💡 生産性の高い環境でスキルを磨く
技術負債の返済やリファクタリングに理解のある企業で働きませんか?自社開発特化の求人を見るなら。

まず、なぜ並行処理が難しいのかを整理しましょう。最大の敵は「メモリ共有」です。

従来の言語では、複数のスレッドが1つの変数をロック(Mutex)を取り合って更新するスタイルが一般的でした。しかし、この方法はロックの粒度設計が難しく、バグを生み出しやすいという欠点があります。特に、ロックの取得順序を間違えるとデッドロックが発生し、システム全体が停止してしまうリスクもあります。

Go言語の設計思想はこれとは真逆です。
「メモリを共有することで通信するな。通信することでメモリを共有せよ」(Don’t communicate by sharing memory, share memory by communicating.)

この思想を具現化したのが channel です。データを「パイプ」に通して受け渡すことで、ロックに頼らずに安全なデータ共有を実現します。CSP(Communicating Sequential Processes)という理論に基づいたこのモデルは、複雑な状態管理をシンプルにし、バグの混入を防ぐ強力な武器となります。

このあたりの基礎的な技術選定やマインドセットについては、技術トレンドについていけないと感じるミドルエンジニアのキャリア再設計でも触れている「基礎技術の深化」に通じる話ですね。

IT女子 アラ美
channelって、ただのデータの通り道だと思ってました。

ITアライグマ
そうですね。でもその「通り道」をどう繋ぐかで、システムの安全性と拡張性が劇的に変わるんです。

基本パターン:パイプラインとファンアウト

では、基本的なパターンを見ていきましょう。これらはGoの標準ライブラリでも多用されている設計パターンです。

パイプライン(Pipeline)

データ処理を「工程」ごとに分け、それらを channel で繋ぐパターンです。各工程(goroutine)が独立して動くため、流れ作業のように効率よくデータを処理できます。各ステージは入力channelからデータを受け取り、加工して出力channelに流すという単純な責務に集中できるため、テストも容易になります。


// 数値を生成する(生産者)
func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// 数値を2倍にする(加工者)
func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // 繋げる
    c := generator(2, 3)
    out := sq(c)

    // 消費する
    for n := range out {
        fmt.Println(n) // 4, 9
    }
}

ファンアウト / ファンイン(Fan-out / Fan-in)

重たい処理を「ファンアウト(複数のgoroutineに分散)」して並列実行し、その結果を「ファンイン(1つのchannelに集約)」するパターンです。

例えば、大量のログファイルを解析する場合、ファイルの読み込みは1つのgoroutineで行い、解析処理はCPUコア数分のワーカーgoroutineに分散させ(ファンアウト)、最後に結果を集計用channelにまとめる(ファンイン)といった構成が取られます。これにより、CPUリソースを最大限に活用できます。

クラウド環境での分散処理にも通じる考え方ですね。オンプレエンジニアがクラウドネイティブ環境に移行するためのスキルギャップ解消戦略でも解説した「非同期アーキテクチャ」の基礎となります。

IT女子 アラ美
なるほど、forループで回すだけじゃなくて、役割分担させるんですね。

ITアライグマ
その通りです。特にI/O待ちが発生する処理では、この分散処理が圧倒的なパフォーマンスを発揮します。

【ケーススタディ】ワーカープール導入でスループット30%向上

📚
年収1,000万円超えのキャリア戦略
高度な並行処理スキルを活かして、GoogleやAmazonレベルの技術環境へ挑戦しませんか?

ここでは、実際に導入した「画像処理バッチ」の改善事例を紹介します。

画像リサイズ・アップロード処理の高速化

状況 (Before)

1万枚の商品画像をリサイズしてS3にアップロードするバッチ処理。当初は for ループで1枚ずつ順次処理していたため、完了までに約50分かかっていました。CPU使用率は低く、ネットワーク待ち時間が大半を占めていました。また、エラーが発生すると処理全体が止まってしまうという課題もありました。

行動 (Action)

「ワーカープールパターン」を導入しました。具体的には、入力画像をchannelに流し込み、固定数(例:CPUコア数 × 4)のワーカーgoroutineがそれを奪い合って処理する構成に変更しました。これにより、goroutineの増殖によるメモリ枯渇を防ぎつつ、並列度を最適化しました。さらに、エラー用のchannelを用意し、エラーが発生しても他のワーカーが止まらないように設計しました。

結果 (After)

処理時間は約15分に短縮され、スループットは3倍以上に向上しました。エラーハンドリングも統一され、運用が安定しました。1件あたり500msかかっていた処理を擬似的に55ms程度(並列度10の場合)まで圧縮できた計算になります。

処理時間の比較

順次処理と並行処理(ワーカープール)の実行時間を比較したグラフです。I/O待ち時間の多いタスクほど、並行処理の効果が顕著に表れます。

順次処理と並行処理の実行時間比較

Dockerコンテナのリソース管理についてはDockhandで始めるセルフホストDocker管理などの記事も参考に、インフラレベルでの最適化も合わせて検討すると良いでしょう。

IT女子 アラ美
速い!でもワーカーの数はどうやって決めるんですか?

ITアライグマ
基本はCPUバウンドならコア数、I/Oバウンドならその数倍〜数十倍が目安です。ベンチマークを取って調整するのが一番確実ですね。

便利なツールとベストプラクティス

最後に、並行処理をより安全に実装するためのツール(パッケージ)と、守るべきルールを紹介します。これらを使うことで、煩雑なチャネル管理から解放されます。

golang.org/x/sync/errgroup

複数のgoroutineをまとめて管理し、「1つでもエラーが出たら全体をキャンセルする」といった制御を簡単に書けるパッケージです。標準の sync.WaitGroup ではエラー伝播が面倒ですが、errgroup ならシンプルに記述できます。


g, ctx := errgroup.WithContext(context.Background())

urls := []string{"http://a.com", "http://b.com"}
for _, url := range urls {
    url := url // 変数のキャプチャに注意(Go 1.22以降は不要)
    g.Go(func() error {
        // コンテキストがキャンセルされていたら即終了
        if ctx.Err() != nil {
            return ctx.Err()
        }
        return fetch(url)
    })
}

if err := g.Wait(); err != nil {
    fmt.Println("Error:", err)
}

Contextによるキャンセル伝播

Goの並行処理において context.Context は必須です。親の処理がタイムアウトしたりキャンセルされたりした場合、生成した全ての子goroutineも速やかに停止させる必要があります。これを怠ると、ゾンビgoroutineがメモリを食いつぶす「goroutineリーク」の原因になります。

開発ツールの選定についてはClaude Codeを拡張する「Antigravity Awesome Skills」入門でも紹介しているような、最新のエコシステムを活用するのも手です。

IT女子 アラ美
Contextってただのおまじないじゃなかったんですね…。

ITアライグマ
はい。並行処理における「・\・~・停止スイッチ」の役割を果たします。全ての関数にContextを渡すのがGoの作法(イディオム)です。

キャリアの選択とステップアップ

並行処理のような高度な技術を習得しても、それを発揮できる環境がなければ宝の持ち腐れです。もし今の現場が「動けばいい」というコードばかり量産していて、リファクタリングや品質向上にリソースを割けないのであれば、環境を変えるタイミングかもしれません。

Go言語をメインで採用している企業は、技術的負債への感度が高く、エンジニアの生産性を重視する傾向にあります。円安時代のエンジニア生存戦略でも触れたように、自身のスキルを正当に評価してくれる場所で働くことは、長期的なキャリア形成において最も重要な戦略の一つです。


さらなる年収アップやキャリアアップを目指すなら、ハイクラス向けの求人に特化した以下のサービスがおすすめです。

比較項目 TechGo レバテックダイレクト ビズリーチ
年収レンジ 800万〜1,500万円ハイクラス特化 600万〜1,000万円IT専門スカウト 700万〜2,000万円全業界・管理職含む
技術スタック モダン環境中心 Web系に強い 企業によりバラバラ
リモート率 フルリモート前提多数 条件検索可能 原則出社も多い
おすすめ度 S技術で稼ぐならここ A受身で探すなら Bマネジメント層向け
公式サイト 無料登録する - -
IT女子 アラ美
年収を上げたいんですが、ハイクラス求人ってハードルが高そうで迷います…
ITアライグマ
技術力を武器に年収を上げたいならTechGo一択!でも、自分の市場価値を幅広くチェックしたいならビズリーチも登録しておくと安心ですよ。

まとめ

Go言語の並行処理は強力ですが、正しく扱うにはパターンを知る必要があります。

  • メモリ共有ではなく通信:channelを使ってデータを安全に受け渡す。
  • パイプラインとファンアウト:処理を分割し、並列化してスループットを上げる。
  • errgroupとContext:エラー制御とキャンセル処理を徹底し、安全性を担保する。

まずは、身近なバッチ処理やAPIの非同期処理で、小さなワーカープールを実装してみることから始めてみてください。その爆速なレスポンスを見れば、もう同期処理には戻れなくなりますよ!

IT女子 アラ美
まずはerrgroupの導入からやってみます!

ITアライグマ
ぜひ!並行処理を制する者はGoを制します。応援しています!

厳しめIT女子 アラ美による解説ショート動画はこちら

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次