Goで学ぶ並行処理パターン：Fan-out/Fan-inパターン

**Go言語では、goroutineやchannelといった標準機能により、シンプルかつパワフルな並行処理の実装が可能です。しかし、実務的な開発では、これらの基本要素を適切に組み合わせて可読性・保守性の高いコードを書くことが求められます。その中でよく使われる並行処理の「設計パターン」の一つにFan-out/Fan-inパターンがあります。**

本記事では、Fan-out/Fan-inパターンを中心に、どのような場面で有効か、また実装例を示しながら解説します。

# Fan-out/Fan-inパターンとは
**Fan-out**は、1つの入力ストリームや入力イベントに対して、「**複数のgoroutineで並行に処理を広げる**」ことを指します。例えば、ある仕事キューからタスクを読み取り、それらを複数のワーカー(goroutine)に振り分けて同時並行で処理する場面がFan-outパターンです。

例：
ユーザーからのリクエストを受け取り、その処理を複数のバックエンドサービスに並行問い合わせする。
複数のファイルを同時にパース・解析する。
CPUコア数に応じて複数のgoroutineで計算タスクを並列実行する。

**Fan-inとは？**
Fan-inは、複数の並行処理（goroutine）からの**結果を一つのチャンネルなどに集約するパターン**です。Fan-outで拡散された処理が終わったら、そのすべての結果をまとめ上げ、後続の処理に渡せます。これにより、並行処理後に一括して結果を処理したり、最終的な集約ステップを行うことが可能になります。

例：
複数の外部API問い合わせ結果を全て受け取り、レスポンスを統合してクライアントに返す。
並行処理されたデータ解析結果をまとめて集計する。
**つまり、Fan-out/Fan-inパターンは「入力(1点) → 並行処理(多点) → 集約(1点)」という一連の流れを上手にモデル化することで、コードを整然とした並行パイプラインに仕上げる手法です。**

# Fan-out/Fan-inパターンの利点
並行処理の分業化：
タスクを複数のgoroutineに分配することで、I/O待機やCPU計算を並行に行い、スループットが向上します。

**スケール性・柔軟性**：
処理負荷が増えた場合、ワーカーgoroutineを増やすことで対処しやすいです。

**コードの明確化**：
入力ステップ(Fan-out)、処理ステップ(ワーカー群)、集約ステップ(Fan-in)が明確に分離されるため、並行フローが理解しやすくなります。

Fan-out/Fan-inパターンの実装例
以下に、簡易的な実装例を示します。ここでは以下の処理をモデルとします。

入力となる一連のタスク（整数値のスライス）を受け取り、それぞれについて「擬似的な重い計算」を並行に行うワーカーを複数稼働させ、結果を最終的に集約する。

```go
package main

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

// 擬似的な重い処理
func heavyWork(n int) int {
	// ランダムな時間待機しているふり
	time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
	return n * n
}

func main() {
	rand.Seed(time.Now().UnixNano())
	inputs := []int{1, 2, 3, 4, 5}

	// Fan-out: inputsをワーカー達に振り分ける
	numWorkers := 3
	inCh := make(chan int)
	outCh := make(chan int)

	// ワーカー起動
	for i := 0; i < numWorkers; i++ {
		go func() {
			for n := range inCh {
				result := heavyWork(n)
				outCh <- result
			}
		}()
	}

	// 入力をinChに投入(Fan-outの開始)
	go func() {
		for _, n := range inputs {
			inCh <- n
		}
		close(inCh)
	}()

	// Fan-in: 複数ワーカーからの結果を集約
	results := make([]int, 0, len(inputs))
	for i := 0; i < len(inputs); i++ {
		res := <-outCh
		results = append(results, res)
	}

	// 全ての結果が集まったので、ここから集約・処理を行う
	fmt.Println("Results:", results)
}
```

コードの解説
入力と出力チャネル：
inChはワーカーへ仕事を渡すためのチャネル、outChはワーカーからの結果を受け取るためのチャネルです。

ワーカー群の起動 (Fan-out)：
for i := 0; i < numWorkers; i++ { ... }で複数のgoroutineを起動し、それぞれがinChから仕事を受け取り処理結果をoutChに送るという単純なロジックを持っています。

入力をinChへ供給：
go func()で別goroutineを起動し、inputsをinChへ順次投入し終わった後にclose(inCh)しています。これによりワーカー側はinChのクローズを感知すると処理終了できます。

出力を受け取り集約 (Fan-in)：
メインgoroutineはlen(inputs)回outChから受信し、すべてのワーカー処理が完了するのを待ちます。この部分がFan-in処理にあたります。

この設計により、ワーカー数や処理負荷を簡単に調整できます。また、処理の流れが明確なので並行処理ロジックが理解しやすく、保守性も向上します。

その他の並行処理パターン
Pipelineパターン：
処理を段階的に分け、複数の段階をチャネルでつなぐことで、データストリームをパイプラインのように流す手法。各ステージをgoroutineで並行処理できるため、段階的に非同期処理を組み上げやすい。

Worker Poolパターン：
Fan-out/Fan-inパターンの一部とも言えるが、一定数のワーカーgoroutineをプールして仕事を割り当てるパターン。スレッドプール的な概念で、リソースの効率的な活用を狙う。

Selectパターン：
複数のチャネルを同時に待ち受けて、到着したデータに応じて処理を行うselect文を活用する。非同期I/Oや複数のイベントを待機して処理を分岐する際に便利。

これらのパターンは、goroutineやchannelの基本に立脚しており、組み合わせることでさらに柔軟でスケーラブルな並行処理設計が可能になります。


# まとめ
Fan-out/Fan-inパターンは、一つの入力を複数goroutineで並行処理し（Fan-out）、結果を一箇所に集約する（Fan-in）設計パターンです。
このパターンを使うことで、スループット向上や柔軟なスケールアウト、コードの見通しの良さを実現できます。
PipelineやWorker Poolなど、Goの並行処理では他にも多くのパターンが存在しますが、すべてはgoroutineとchannelという基本要素の理解から始まります。
Fan-out/Fan-inパターンを活用して、より効果的な並行処理設計を行ってみてください！