チャネル

14. チャネル

例え話

コンベアベルトを想像してください:一方の端で人がパッケージを置き、もう一方の端で別の人が取り出します。

これがチャネルの本質です——goroutine間の安全な通信のためのパイプです。


コアコンセプト

コンセプト 説明
チャネル 型付き通信パイプ、chanキーワードで宣言
送信 ch <- valueでチャネルにデータを書き込む
受信 value := <-chでチャネルからデータを読み取る
バッファなしチャネル 同期通信;送信側と受信側が同時に準備完了している必要がある
バッファ付きチャネル 非同期通信;バッファが満杯になるまで送信はブロックされない
close チャネルを閉じる;これ以上の送信は許可されない
range チャネルが閉じられるまでチャネルから継続的に受信する
方向制限 送信専用chan<-または受信専用<-chan

Goの哲学: メモリを共有して通信するのではなく、通信によってメモリを共有する。


基本構文と使い方

チャネルの作成

GO
// バッファなしチャネル(同期)
ch := make(chan int)

// バッファ付きチャネル(非同期、バッファサイズ5)
ch := make(chan string, 5)

送信と受信

GO
ch := make(chan int)

// 送信(別のgoroutine内で)
go func() {
    ch <- 42 // チャネルに42を送信
}()

// 受信
value := <-ch // チャネルから値を受信、データが利用可能になるまでブロック
fmt.Println(value) // 42

チャネルを閉じる

GO
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // チャネルを閉じる、これ以上の送信は許可されない

rangeでの受信

GO
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)

// rangeはチャネルが閉じられるまで受信を続ける
for v := range ch {
    fmt.Println(v) // 10 20 30
}

方向制限

GO
// 送信専用チャネル
func producer(ch chan<- int) {
    ch <- 100
}

// 受信専用チャネル
func consumer(ch <-chan int) {
    v := <-ch
    fmt.Println(v)
}

💡 ヒント


例:基本的なgoroutine通信(難易度⭐)

GO
package main

import "fmt"

func main() {
    // バッファなしチャネルを作成
    ch := make(chan string)

    // メッセージを送信するgoroutineを起動
    go func() {
        ch <- "こんにちは、メインスレッド!"
    }()

    // メインgoroutineがメッセージを受信(待機中にブロック)
    msg := <-ch
    fmt.Println(msg) // こんにちは、メインスレッド!
}
▶ 試してみよう

説明: バッファなしチャネルは2つのgoroutineを同期します——送信側は受信側が準備完了するまでブロックします。


例:プロデューサー・コンシューマーパターン(難易度⭐⭐)

GO
package main

import "fmt"

// プロデューサー:データを生成してチャネルに送信(送信専用)
func producer(id int, ch chan<- int, count int) {
    for i := 0; i < count; i++ {
        value := id*100 + i
        fmt.Printf("プロデューサー %d:%d を送信中\n", id, value)
        ch <- value
    }
}

// コンシューマー:チャネルからデータを受信して処理(受信専用)
func consumer(id int, ch <-chan int) {
    for v := range ch {
        fmt.Printf("コンシューマー %d:%d を処理中\n", id, v)
    }
    fmt.Printf("コンシューマー %d:チャネルが閉じられ、終了します\n", id)
}

func main() {
    // バッファ付きチャネルを作成
    ch := make(chan int, 5)

    // 2つのプロデューサーを起動
    go producer(1, ch, 3)
    go producer(2, ch, 3)

    // 2つのコンシューマーを起動
    go consumer(1, ch)
    go consumer(2, ch)

    // すべてのプロデューサーが完了するまで待機(実際のプロジェクトではsync.WaitGroupを使用)
    // ここではデモンストレーションのために単純にsleepを使用
    import_time := time.After(2 * time.Second)
    <-import_time

    close(ch) // チャネルを閉じる

    // コンシューマーが残りのデータを処理する時間を与える
    time.Sleep(500 * time.Millisecond)
    fmt.Println("すべての作業が完了しました")
}
▶ 試してみよう

説明:


例:チャネルによるセマフォとFan-out/Fan-in(難易度⭐⭐⭐)

GO
package main

import (
    "fmt"
    "sync"
    "time"
)

// workerはタスクを処理し、結果をresultsチャネルに送信する
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("ワーカー %d:タスク %d の処理を開始\n", id, job)
        time.Sleep(time.Duration(100+id*50) * time.Millisecond) // 作業をシミュレート
        result := job * job
        fmt.Printf("ワーカー %d:タスク %d 完了、結果 %d\n", id, job, result)
        results <- result
    }
}

func main() {
    numJobs := 10
    numWorkers := 3

    jobs := make(chan int, numJobs)     // タスクチャネル
    results := make(chan int, numJobs)  // 結果チャネル

    // ワーカープールを起動
    var wg sync.WaitGroup
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // タスクを送信
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs) // タスクチャネルを閉じる;ワーカーのrangeループが終了する

    // すべてのワーカーが完了したらresultsチャネルを閉じる
    go func() {
        wg.Wait()
        close(results)
    }()

    // すべての結果を収集
    total := 0
    for r := range results {
        total += r
    }

    fmt.Printf("すべての結果の合計:%d\n", total)
}
▶ 試してみよう

説明:


実践的な使用例

ケース1:タイムアウト制御

GO
package main

import (
    "fmt"
    "time"
)

func slowOperation(ch chan<- string) {
    time.Sleep(3 * time.Second) // 遅い操作をシミュレート
    ch <- "操作完了"
}

func main() {
    ch := make(chan string, 1)
    go slowOperation(ch)

    // selectを使用してタイムアウト制御
    select {
    case result := <-ch:
        fmt.Println("結果を受信:", result)
    case <-time.After(2 * time.Second):
        fmt.Println("操作がタイムアウトしました!") // 2秒後にトリガー
    }
}

説明: time.Afterは指定された時間後に値を送信するチャネルを返します。selectと組み合わせることで、エレガントなタイムアウト制御が可能になります。


ケース2:並列クローラーのレート制限

GO
package main

import (
    "fmt"
    "time"
)

// fetchURLはURLの取得をシミュレートする
func fetchURL(url string) string {
    time.Sleep(500 * time.Millisecond) // ネットワーク遅延をシミュレート
    return "ページコンテンツ:" + url
}

func main() {
    urls := []string{
        "https://example.com/page1",
        "https://example.com/page2",
        "https://example.com/page3",
        "https://example.com/page4",
        "https://example.com/page5",
    }

    // バッファ付きチャネルをセマフォとして使用し、同時実行数を2に制限
    semaphore := make(chan struct{}, 2)
    results := make(chan string, len(urls))

    for _, url := range urls {
        go func(u string) {
            semaphore <- struct{}{} // セマフォを取得(満杯の場合はブロック)
            fmt.Printf("取得開始:%s\n", u)
            result := fetchURL(u)
            results <- result
            <-semaphore // セマフォを解放
        }(url)
    }

    // 結果を収集
    for i := 0; i < len(urls); i++ {
        fmt.Println(<-results)
    }

    fmt.Println("すべてのページの取得が完了しました")
}

説明: バッファ付きチャネルがセマフォとして機能し、同時実行するgoroutineの数を制限して、過剰な同時リクエストが対象サーバーに負荷を与えるのを防ぎます。


❓ よくある質問

Q1:閉じられたチャネルにデータを送信するとどうなりますか?

パニックが発生します。 閉じられたチャネルへの送信操作はpanic: send on closed channelを引き起こします。受信はパニックを引き起こしませんが、ゼロ値を返します。

GO
ch := make(chan int, 1)
ch <- 1
close(ch)
// ch <- 2 // ❌ panic: send on closed channel
v := <-ch   // ✅ 1を返す(バッファ内の残りの値)
v2 := <-ch  // ✅ 0を返す(ゼロ値、チャネルは空で閉じられている)

ベストプラクティス: 送信側がチャネルを閉じるべきであり、受信側ではありません。複数の送信側がチャネルを共有する場合は、sync.Onceまたは追加のdoneチャネルを使用して閉じる操作を調整します。


Q2:nilチャネルはどうなりますか?

送信と受信の両方が永久にブロックされ、nilチャネルを閉じるとパニックが発生します。

GO
var ch chan int // nilチャネル

// ch <- 1    // ❌ 永久にブロック
// v := <-ch  // ❌ 永久にブロック
// close(ch)  // ❌ panic: close of nil channel

nilチャネルはselectで有用です——対応するcaseは自動的にスキップされます:

GO
var ch chan int // nilチャネル
select {
case v := <-ch:       // 無視される(nilチャネル)
    fmt.Println(v)
case <-time.After(1 * time.Second):
    fmt.Println("タイムアウトしました")
}

Q3:バッファなしチャネルとバッファ付きチャネルの違いは何ですか?

特性 バッファなし make(chan T) バッファ付き make(chan T, n)
送信がブロックされる時 受信側が準備できていない バッファが満杯
受信がブロックされる時 送信側が準備できていない バッファが空
同期性 同期(ランデブー) 非同期(バッファが満杯になるまで)
典型的な用途 シグナル通知、同期 プロデューサー・コンシューマー、キュー

Q4:チャネルが閉じられているかを確認するには?

受信操作の2番目の戻り値を使用します:

GO
ch := make(chan int, 1)
ch <- 42
close(ch)

v, ok := <-ch
fmt.Println(v, ok) // 42 true

v2, ok := <-ch
fmt.Println(v2, ok) // 0 false(閉じられていてデータなし)

チャネルが閉じられていてバッファにデータが残っていない場合、okfalseになります。


📖 まとめ

重要ポイント 詳細
チャネルの作成 バッファなしはmake(chan T)、バッファ付きはmake(chan T, n)
送信 ch <- value、ブロック条件はバッファタイプに依存
受信 value := <-chまたはv, ok := <-ch
閉じる close(ch)、送信側の責任、閉じた後は送信不可
range for v := range chは閉じられるまで受信を続ける
方向制限 chan<-は送信専用、<-chanは受信専用
select マルチプレキシング、複数のチャネル操作を処理
ベストプラクティス goroutineの完了待機にはsync.WaitGroupを優先使用;二重閉じを回避

チャネルはGoの並行処理のコアです。 バッファなしとバッファ付きの違い、close/range、方向制限、select(次のレッスン)を理解すれば、Goの並列通信の本質をマスターしたことになります。


📝 演習

演習1:チャネルの基本

3つのgoroutineを起動し、各goroutineが数値の二乗を計算してチャネルに送信するプログラムを書いてください。メインgoroutineがすべての結果を受信して出力します。

要件:


演習2:チャネルによるFan-inの実装

複数の受信専用チャネルを1つの受信専用チャネルにマージする関数merge(channels ...<-chan int) <-chan intを書いてください。

要件:


演習3:タイムアウト付きワーカープール

以下の機能を持つワーカープールを実装してください:

ヒント: チャネル、select、time.Afterを組み合わせてください。


次のレッスン

15. Select →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%