チャネル
ゴルーチンの間で値を渡す
Go
ch := make(chan int) // int を流すチャネル
ch <- 42 // 入れる
v := <-ch // 取り出す矢印が向きを表します。ch <- はチャネルへ入れる、<-ch はチャネルから取り出す、です。矢印はいつも値の流れる向きを指しています。
待ち合わせの道具でもある
チャネルの本当の役目は、値を運ぶことだけではありません。タイミングを合わせることです。
Go
v := <-chこの行は、値が入るまでそこで止まります。入った瞬間に動き出します。
前のレッスンで「待たないと結果が消える」問題がありました。チャネルから取り出す行を書けば、そこで待つので消えません。
容量のあるチャネル
Go
ch := make(chan int) // 容量0
ch := make(chan int, 3) // 容量3容量0のチャネルは、入れる側も取り出す側が来るまで待ちます。手渡しに近い動きです。
Go
ch := make(chan int)
ch <- 42 // 取り出す人がいないと、ここで永久に止まる1人で書いていると、これで固まります。
fatal error: all goroutines are asleep - deadlock!デッドロックです。全員が誰かを待っていて、誰も進めない状態を Go が検出して教えてくれます。
容量があれば、その数までは待たずに入れられます。
Go
ch := make(chan int, 3)
ch <- 1 // 待たない
ch <- 2 // 待たない前のレッスンのひな形で、要素数と同じ容量の入れ物を用意したのは、これが理由です。受信が始まる前でも全件を送信できるので、ゴルーチンは結果を置いて終了できます。
取り出す回数を合わせる
最も間違えやすいのがここです。
入れた数 > 取り出す数 結果が足りない
入れた数 < 取り出す数 永久に待つ。タイムアウト送る件数を数えて、同じ回数だけ取り出します。受信する時点でまだ値が届いていなければ、その行で待ちます。容量があるチャネルでも、空になった後の受信は待つので、受信回数を増やしてはいけません。
たとえば、3つの作業の完了を文字列で知らせる場合です。結果の数値を集計する演習とは別に、届いた通知を1件ずつ表示してみます。
Go
notices := make(chan string, 3)
go func() { notices <- "画像の準備完了" }()
go func() { notices <- "音声の準備完了" }()
go func() { notices <- "字幕の準備完了" }()
for remaining := 3; remaining > 0; remaining-- {
message := <-notices
fmt.Println(message)
}3件送るので3回受け取ります。表示の順番は決まりませんが、3回の受信が終われば、全作業が通知を送るところまで進んだと分かります。2回だけでは通知が残り、4回目を待つと誰も送らないので止まります。
前の回の collectResults も、送信件数に合わせて受信しています。この回では、その回収するループを自分で書きます。次のレッスンでは、値を受け取らずに完了を待つ WaitGroup を学びます。
range で受け取る
Go
for v := range ch {
total += v
}チャネルが閉じられるまで受け取り続けます。閉じるのは送る側です。
Go
close(ch)閉じ忘れると、range は永久に待ちます。入門のうちは回数を数えて取り出す形で十分なので、close は「そういうものがある」と知っておく程度で構いません。
要件
- 準備済みの startPriceSums で前半と後半の集計を始める
- 2回繰り返すループを parallelSum に書く
- ループの中でチャネルから1件受信して total に加える
- 2件とも受信してから合計を返す。価格が0件でも同じ回数だけ受信する