Goのゴルーチンとチャネル:実践的な並行処理ガイド

Backend2026-09-30TryQuickToolBox

おそらく、動作していたGoコードが突然動かなくなった経験があるでしょう。負荷がかかるとデータ競合でサービスがクラッシュしたり、デッドロックでAPIがフリーズしたりするかもしれません。並行処理はGoの最大の売りの一つですが、多くの開発者がつまずくところでもあります。ゴルーチンとチャネルはその構成要素ですが、正しく使うにはそれらの動作とよくある落とし穴を理解する必要があります。

このガイドでは、ゴルーチンとチャネルを使った実践的なパターンを、すぐに適用できるコード例とアドバイスと共に紹介します。

ゴルーチンとは?

ゴルーチンは、Goランタイムによって管理される軽量スレッドです。関数呼び出しの前にgoを付けることで作成します:

go doSomething()

ゴルーチンは小さなスタック(数キロバイト)で開始し、必要に応じて成長します。これにより、数千、あるいは数百万のゴルーチンを安価に生成できます。GoスケジューラはゴルーチンをOSスレッドに多重化するため、スレッドを直接管理する必要はありません。

しかし、調整なしにゴルーチンを生成するのは混乱を招くレシピです。それらを待機したり、データを渡したり、キャンセルしたりする方法が必要です。そこでチャネルの出番です。

チャネル:ゴルーチン間の通信

チャネルは、値を送受信するための型付きの導管です。同期を提供します:送信は受信側が準備できるまでブロックし、受信は値が利用可能になるまでブロックします。これにより、チャネルはゴルーチンの調整に理想的です。

make(chan T)でチャネルを作成します:

ch := make(chan int)
go func() {
    ch <- 42 // 送信
}()
value := <-ch // 受信

デフォルトでは、チャネルはバッファなしであり、送信と受信が同期して行われます。バッファ付きチャネルでは、受信側なしで限られた数の送信が可能です:

ch := make(chan int, 3) // バッファサイズ3
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 は誰かが受信するまでブロックする

バッファ付きチャネルはバーストを平滑化できますが、調整の必要性を排除するものではありません。

一般的な並行処理パターン

1. ワーカープール

固定数のワーカーにタスクを分散してリソース使用量を制限します:

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for i := 1; i <= 5; i++ {
        jobs <- i
    }
    close(jobs)

    for r := 1; r <= 5; r++ {
        <-results
    }
}

このパターンは、並列性を制御したいCPUバウンドまたはIOバウンドのタスクに最適です。

2. ファンアウト、ファンイン

ファンアウトは複数のゴルーチンが同じチャネルから読み取ることを意味します。ファンインは複数のチャネルからの結果を1つにマージすることを意味します:

func fanIn(chs ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    wg.Add(len(chs))
    for _, ch := range chs {
        go func(c <-chan int) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

複数のプロデューサーがあり、単一の結果ストリームが欲しい場合に使用します。

3. 多重化のためのselect

selectステートメントは、ゴルーチンが複数のチャネル操作を待機できるようにします:

select {
case msg := <-ch1:
    fmt.Println("ch1から受信:", msg)
case ch2 <- 42:
    fmt.Println("ch2に送信")
case <-time.After(1 * time.Second):
    fmt.Println("タイムアウト")
}

非ブロッキング操作にはデフォルトケース付きのselectを、永遠に待つのを避けるにはタイムアウト付きで使用します。

落とし穴とその回避方法

ベストプラクティス

比較:チャネル vs. ミューテックス

側面チャネルミューテックス
ユースケースデータの受け渡し、調整共有状態の保護
複雑さ単純なタスクでは高い単純なタスクでは低い
パフォーマンス同期によるオーバーヘッド短いクリティカルセクションでは通常高速
リスクデッドロック、リーク注意しないとデッドロック

FAQ

バッファ付きチャネルはいつ使うべきですか?

バーストを処理するために送信側と受信側を分離したい場合、または送信回数が正確にわかっていてブロックを避けたい場合にバッファ付きチャネルを使用します。ただし、バッファなしチャネルはより強力な同期保証を提供し、しばしば推論が容易です。

ゴルーチンリークを避けるには?

すべてのゴルーチンが明確な終了パスを持つようにしてください。context.Contextを使用してキャンセルを通知し、これ以上値が送信されない場合はチャネルを閉じ、タイムアウトやデフォルトケースなしでチャネル操作で無期限にブロックしないようにします。

すべてにチャネルを使えますか?

いいえ。チャネルは強力ですが、オーバーヘッドと複雑さを追加します。単純な共有状態には、ミューテックスの方がしばしば簡単で効率的です。Goの格言に「メモリを共有して通信するな。通信してメモリを共有せよ」とありますが、それはチャネルが常に答えであることを意味しません。

並行処理の問題をデバッグしたり、Goサービスのログを分析したりする場合、Nginx Log Analyzerがパターンやエラーを素早く見つけるのに役立ちます。