Goのチャンネル実践講座：Webステータスチェッカーで学ぶ並行処理の本質


Goのチャンネルは単なるデータ送受信の道具ではなく、ゴルーチン間の**安全な通信と同期**を実現する核となる機能です。実際のアプリケーション開発で「なぜチャンネルが必要か」「使わないとどうなるか」を学習する備忘録となっています。

---

### ステップ1: チャンネルなし実装（問題点の顕在化）
まずはチャンネルを使わない実装から始め、問題点を明確にします。

```go
package main

import (
    "fmt"
    "net/http"
    "time"
)

func checkStatus(url string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("%s: エラー %v\n", url, err)
        return
    }
    defer resp.Body.Close()
    fmt.Printf("%s: %d (処理時間: %v)\n", url, resp.StatusCode, time.Since(start))
}

func main() {
    urls := []string{
        "https://www.google.com",
        "https://www.github.com",
        "https://www.example.com",
        "https://invalid-domain-12345.com",
    }

    for _, url := range urls {
        go checkStatus(url)
    }

    // ゴルーチンの完了を待たない
    time.Sleep(2 * time.Second) // 不正確な待機時間
}
```

**問題点:**
1. **結果の収集不可**: 各ゴルーチンが個別に出力するため、結果を集約できない
2. **早期終了のリスク**: `Sleep`による待機は非効率で、実際の処理時間を予測できない
3. **エラーハンドリング困難**: 個々のゴルーチンのエラーを一元的に処理できない

> 💡 **なぜチャンネルが必要か？**  
> ゴルーチンは「並行」だが「非同期」。実行完了を検知する仕組みがないと、メイン関数が終了してプロセス全体が停止する危険があります。

---

### ステップ2: 基本チャンネル導入（同期の実現）
結果収集と同期のためにチャンネルを導入します。

```go
func checkStatus(url string, ch chan<- string) {
    start := time.Now()
    resp, err := http.Get(url)
    if err != nil {
        ch <- fmt.Sprintf("%s: エラー %v", url, err)
        return
    }
    defer resp.Body.Close()
    ch <- fmt.Sprintf("%s: %d (処理時間: %v)", url, resp.StatusCode, time.Since(start))
}

func main() {
    urls := []string{ /* 同上 */ }
    ch := make(chan string)

    for _, url := range urls {
        go checkStatus(url, ch)
    }

    // 結果を順次受信
    for range urls {
        fmt.Println(<-ch)
    }
}
```

**改善点:**
- **同期の保証**: ゴルーチンの数だけ受信することで確実に完了を待機
- **結果の集約**: チャンネル経由で結果を一元的に収集可能
- **タイムアウト不要**: 確実に全処理を待つためSleepが不要に

> 🚀 **チャンネルの本質1: 同期ポイント**  
> チャンネルはデータ転送だけでなく、ゴルーチンの実行完了を通知する「同期ポイント」として機能します。

---

### ステップ3: バッファ付きチャンネル（パフォーマンス向上）
基本形の問題点である「送信時のブロッキング」を解決します。

```go
func main() {
    urls := []string{ /* 同上（20サイトに拡大） */ }
    ch := make(chan string, len(urls)) // バッファサイズ=URL数

    for _, url := range urls {
        go checkStatus(url, ch)
    }

    for range urls {
        fmt.Println(<-ch)
    }
}
```

**バッファなし vs バッファあり:**
| 特性 | バッファなし | バッファあり |
|------|--------------|--------------|
| 送信動作 | 受信準備ができるまでブロック | バッファが満杯まで非ブロック |
| 使用例 | 厳密な同期が必要な場合 | 並列性を最大化したい場合 |
| パフォーマンス | 送受信のタイミングが完全同期 | 受信が遅れても送信を継続可能 |

> 🚀 **チャンネルの本質2: 生産者-消費者パターン**  
> バッファ付きチャンネルは、生産者（ゴルーチン）と消費者（メイン）の速度差を吸収する「バッファ層」として機能します。

---

### ステップ4: selectによる高度な制御（タイムアウト処理）
チャンネルをselectと組み合わせて、実用的な制御を追加します。

```go
func main() {
    ch := make(chan string, len(urls))
    timeout := time.After(3 * time.Second) // 全体タイムアウト

    for _, url := range urls {
        go checkStatus(url, ch)
    }

    for i := 0; i < len(urls); i++ {
        select {
        case result := <-ch:
            fmt.Println(result)
        case <-timeout:
            fmt.Println("エラー: タイムアウトしました")
            return
        }
    }
}
```

**selectの動作原理:**
1. 複数のチャンネル操作を待機
2. 最初に準備できたケースを実行
3. `default`句があれば、どのケースも準備できていない時に実行

> 🚀 **チャンネルの本質3: イベント駆動処理**  
> selectはチャンネルを「イベントソース」として扱い、タイムアウトやキャンセルなどの複雑な制御を可能にします。

---

### ステップ5: チャンネル閉じのベストプラクティス
リソースリークを防ぐための正しいクローズ処理。

```go
func main() {
    ch := make(chan string, len(urls))
    defer close(ch) // 確実にクローズ

    // ゴルーチン起動...

    for result := range ch { // クローズまで自動でループ
        fmt.Println(result)
        if len(ch) == 0 { // 追加: 早期終了条件の例
            break
        }
    }
}
```

**正しいクローズのタイミング:**
- 送信側が閉じる（受信側が閉じるのはアンチパターン）
- `defer`で確実に実行
- クローズ後も受信は可能（ゼロ値が取得される）

---

### チャンネルを使わないとどうなるか？ 実際の障害例

1. **データ競合 (Data Race)**
```go
var results []string // 共有リソース

func checkStatus(url string) {
    // ...処理...
    results = append(results, result) // 非同期アクセス→競合状態
}
```
👉 `go run -race`で検出可能な危険な状態

2. **ゴルーチンリーク**
```go
func leak() {
    ch := make(chan int)
    go func() {
        // 受信者がいないため永遠にブロック
        <-ch 
    }()
    // チャンネルを使わないと回収不能
}
```
👉 メモリリークの原因に

3. **非決定性の動作**
```go
time.Sleep(1 * time.Second) // 実行環境によって動作が変わる
```
👉 タイミング依存はバグの温床

---

### 応用パターン：パイプライン処理

チャンネルの真価は複数ステージのパイプライン構成で発揮されます。

```go
// ステージ1: URLチェック
func checkStage(urls <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        for url := range urls {
            // ...チェック処理...
            out <- result
        }
        close(out)
    }()
    return out
}

// ステージ2: 結果解析
func analyzeStage(results <-chan string) <-chan Report {
    out := make(chan Report)
    go func() {
        for res := range results {
            // ...解析処理...
            out <- report
        }
        close(out)
    }()
    return out
}

func main() {
    urls := generateURLs(100) // URL生成
    results := checkStage(urls)
    reports := analyzeStage(results)
    
    for r := range reports {
        fmt.Printf("レポート: %+v\n", r)
    }
}
```

---

### チャンネル選択の判断基準

| 状況 | 推奨方法 |
|------|----------|
| 単純な同期 | バッファなしチャンネル |
| 高負荷な並行処理 | バッファ付きチャンネル |
| タイムアウト/キャンセル必要 | select + context.Context |
| ストリーム処理 | パイプラインパターン |
| イベント通知 | struct{}型チャンネル |

> 「チャンネルはGoの並行処理における接着剤である」 - Rob Pike  
> データフローと制御フローを安全に結びつける抽象化レイヤーとして理解しましょう。

チャンネルを理解すると、分散システムの構築、リアルタイム処理、効率的なリソース管理など、より高度な並行処理パターンに発展できます。