0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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


ステップ1: チャンネルなし実装(問題点の顕在化)

まずはチャンネルを使わない実装から始め、問題点を明確にします。

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: 基本チャンネル導入(同期の実現)

結果収集と同期のためにチャンネルを導入します。

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: バッファ付きチャンネル(パフォーマンス向上)

基本形の問題点である「送信時のブロッキング」を解決します。

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と組み合わせて、実用的な制御を追加します。

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: チャンネル閉じのベストプラクティス

リソースリークを防ぐための正しいクローズ処理。

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)
var results []string // 共有リソース

func checkStatus(url string) {
    // ...処理...
    results = append(results, result) // 非同期アクセス→競合状態
}

👉 go run -raceで検出可能な危険な状態

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

👉 メモリリークの原因に

  1. 非決定性の動作
time.Sleep(1 * time.Second) // 実行環境によって動作が変わる

👉 タイミング依存はバグの温床


応用パターン:パイプライン処理

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

// ステージ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
データフローと制御フローを安全に結びつける抽象化レイヤーとして理解しましょう。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?