お疲れ様です!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女子 アラ美基本パターン:パイプラインとファンアウト
では、基本的なパターンを見ていきましょう。これらは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リソースを最大限に活用できます。
クラウド環境での分散処理にも通じる考え方ですね。オンプレエンジニアがクラウドネイティブ環境に移行するためのスキルギャップ解消戦略でも解説した「非同期アーキテクチャ」の基礎となります。



【ケーススタディ】ワーカープール導入でスループット30%向上
ここでは、実際に導入した「画像処理バッチ」の改善事例を紹介します。
画像リサイズ・アップロード処理の高速化
状況 (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管理などの記事も参考に、インフラレベルでの最適化も合わせて検討すると良いでしょう。



便利なツールとベストプラクティス
最後に、並行処理をより安全に実装するためのツール(パッケージ)と、守るべきルールを紹介します。これらを使うことで、煩雑なチャネル管理から解放されます。
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」入門でも紹介しているような、最新のエコシステムを活用するのも手です。



キャリアの選択とステップアップ
並行処理のような高度な技術を習得しても、それを発揮できる環境がなければ宝の持ち腐れです。もし今の現場が「動けばいい」というコードばかり量産していて、リファクタリングや品質向上にリソースを割けないのであれば、環境を変えるタイミングかもしれません。
Go言語をメインで採用している企業は、技術的負債への感度が高く、エンジニアの生産性を重視する傾向にあります。円安時代のエンジニア生存戦略でも触れたように、自身のスキルを正当に評価してくれる場所で働くことは、長期的なキャリア形成において最も重要な戦略の一つです。
さらなる年収アップやキャリアアップを目指すなら、ハイクラス向けの求人に特化した以下のサービスがおすすめです。
| 比較項目 | TechGo | レバテックダイレクト | ビズリーチ |
|---|---|---|---|
| 年収レンジ | 800万〜1,500万円ハイクラス特化 | 600万〜1,000万円IT専門スカウト | 700万〜2,000万円全業界・管理職含む |
| 技術スタック | モダン環境中心 | Web系に強い | 企業によりバラバラ |
| リモート率 | フルリモート前提多数 | 条件検索可能 | 原則出社も多い |
| おすすめ度 | 技術で稼ぐならここ | A受身で探すなら | Bマネジメント層向け |
| 公式サイト | 無料登録する | - | - |



まとめ
Go言語の並行処理は強力ですが、正しく扱うにはパターンを知る必要があります。
- メモリ共有ではなく通信:channelを使ってデータを安全に受け渡す。
- パイプラインとファンアウト:処理を分割し、並列化してスループットを上げる。
- errgroupとContext:エラー制御とキャンセル処理を徹底し、安全性を担保する。
まずは、身近なバッチ処理やAPIの非同期処理で、小さなワーカープールを実装してみることから始めてみてください。その爆速なレスポンスを見れば、もう同期処理には戻れなくなりますよ!













