チャネル
14. チャネル
例え話
コンベアベルトを想像してください:一方の端で人がパッケージを置き、もう一方の端で別の人が取り出します。
- コンベアベルトがチャネル
- パッケージを置くのが送信(
ch <- value) - パッケージを取り出すのが受信(
<-ch) - ベルトが満杯の時、送信側は待機し、空の時は受信側が待機する必要があります
これがチャネルの本質です——goroutine間の安全な通信のためのパイプです。
コアコンセプト
| コンセプト | 説明 |
|---|---|
| チャネル | 型付き通信パイプ、chanキーワードで宣言 |
| 送信 | ch <- valueでチャネルにデータを書き込む |
| 受信 | value := <-chでチャネルからデータを読み取る |
| バッファなしチャネル | 同期通信;送信側と受信側が同時に準備完了している必要がある |
| バッファ付きチャネル | 非同期通信;バッファが満杯になるまで送信はブロックされない |
| close | チャネルを閉じる;これ以上の送信は許可されない |
| range | チャネルが閉じられるまでチャネルから継続的に受信する |
| 方向制限 | 送信専用chan<-または受信専用<-chan |
Goの哲学: メモリを共有して通信するのではなく、通信によってメモリを共有する。
基本構文と使い方
チャネルの作成
// バッファなしチャネル(同期)
ch := make(chan int)
// バッファ付きチャネル(非同期、バッファサイズ5)
ch := make(chan string, 5)
送信と受信
ch := make(chan int)
// 送信(別のgoroutine内で)
go func() {
ch <- 42 // チャネルに42を送信
}()
// 受信
value := <-ch // チャネルから値を受信、データが利用可能になるまでブロック
fmt.Println(value) // 42
チャネルを閉じる
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // チャネルを閉じる、これ以上の送信は許可されない
rangeでの受信
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
// rangeはチャネルが閉じられるまで受信を続ける
for v := range ch {
fmt.Println(v) // 10 20 30
}
方向制限
// 送信専用チャネル
func producer(ch chan<- int) {
ch <- 100
}
// 受信専用チャネル
func consumer(ch <-chan int) {
v := <-ch
fmt.Println(v)
}
💡 ヒント
- バッファなしチャネルは送信側と受信側の同期を保証します——データは直接一方から他方に転送されます
- バッファ付きチャネルはバッファが満杯の時に送信でブロックし、バッファが空の時に受信でブロックします
- 閉じられたチャネルへの送信はパニックを引き起こします
- 閉じられたチャネルからの受信はゼロ値を返し、ブロックしません
- 受信側からチャネルを閉じないでください;送信側が閉じるべきです(送信側が1つの場合を除く)
v, ok := <-chを使用してチャネルが閉じられているかを確認します(閉じられていてデータが残っていない場合、okはfalse)
例
例:基本的なgoroutine通信(難易度⭐)
package main
import "fmt"
func main() {
// バッファなしチャネルを作成
ch := make(chan string)
// メッセージを送信するgoroutineを起動
go func() {
ch <- "こんにちは、メインスレッド!"
}()
// メインgoroutineがメッセージを受信(待機中にブロック)
msg := <-ch
fmt.Println(msg) // こんにちは、メインスレッド!
}
説明: バッファなしチャネルは2つのgoroutineを同期します——送信側は受信側が準備完了するまでブロックします。
例:プロデューサー・コンシューマーパターン(難易度⭐⭐)
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("すべての作業が完了しました")
}
説明:
- バッファ付きチャネルは、バッファが満杯でない限り、コンシューマーを待たずにプロデューサーが生産を続けることを可能にします
range chはチャネルが閉じられるまで受信を続けます- 方向制限
chan<-と<-chanはコンパイル時に誤用を防止します
例:チャネルによるセマフォとFan-out/Fan-in(難易度⭐⭐⭐)
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)
}
説明:
- Fan-out:複数のワーカーが同じjobsチャネルから読み取る
- Fan-in:複数のワーカーが同じresultsチャネルに書き込む
WaitGroupはすべてのワーカーが完了してからresultsチャネルを閉じることを保証する- これはGoで一般的な並列ワーカープールパターンです
実践的な使用例
ケース1:タイムアウト制御
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:並列クローラーのレート制限
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を引き起こします。受信はパニックを引き起こしませんが、ゼロ値を返します。
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チャネルを閉じるとパニックが発生します。
var ch chan int // nilチャネル
// ch <- 1 // ❌ 永久にブロック
// v := <-ch // ❌ 永久にブロック
// close(ch) // ❌ panic: close of nil channel
nilチャネルはselectで有用です——対応するcaseは自動的にスキップされます:
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番目の戻り値を使用します:
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(閉じられていてデータなし)
チャネルが閉じられていてバッファにデータが残っていない場合、okはfalseになります。
📖 まとめ
| 重要ポイント | 詳細 |
|---|---|
| チャネルの作成 | バッファなしは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がすべての結果を受信して出力します。
要件:
- バッファなしチャネルを使用
WaitGroupですべてのgoroutineが完了することを確認- 出力は次のようになること:
Results: [1, 4, 9]
演習2:チャネルによるFan-inの実装
複数の受信専用チャネルを1つの受信専用チャネルにマージする関数merge(channels ...<-chan int) <-chan intを書いてください。
要件:
- 入力:複数の
<-chan int - 出力:すべての入力チャネルのデータを含む1つの
<-chan int - goroutineと
WaitGroupを使用して実装 - すべての入力チャネルが閉じられると、出力チャネルも閉じる
演習3:タイムアウト付きワーカープール
以下の機能を持つワーカープールを実装してください:
- N個のワーカーgoroutineを起動
- 各タスクには500msのタイムアウト
- タスクがタイムアウトした場合、ログを記録してスキップ
- 成功したタスクとタイムアウトしたタスクをカウント
ヒント: チャネル、select、time.Afterを組み合わせてください。



