3秒でわかる
並行に動く処理どうしがデータを受け渡すための通り道。共有変数を直接触らせず、受け渡しの順序と待ち合わせをまとめて任せるために使います。
もう少し詳しく
どういうものか
チャンネルは、並行して動く処理の間でデータを流す通り道です。Go では言語の機能として組み込まれていて、goroutine どうしが値を送受信する土管として使います。送る側は ch <- v、受け取る側は v := <-ch と書き、相手が準備できるまで自動的に待ちます。この「待つ」動作が組み込まれている点が、単なるキューとの違いです。
Unix のパイプや、メッセージングの pub/sub も同じ発想です。なお Slack や Discord で言うチャンネルは「話題ごとの部屋」という別の意味なので、文脈で読み分けます。
なぜ必要か
複数の処理が同じ変数を書き換えると、書き込みが衝突して結果が壊れます。ロックで守る方法もありますが、どこで取ってどこで解放するかを人が管理する必要があり、取り忘れやデッドロックの温床になります。チャンネルは「データを共有せず、データを渡す」形にして、所有権を一度に一人へ限定します。Go の標語である「共有メモリで通信するのではなく、通信でメモリを共有する」はこの考え方を指しています。
具体例
package main
import "fmt"
func worker(id int, jobs <-chan int, results chan<- int) {
for n := range jobs {
results <- n * n
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for n := 1; n <= 5; n++ {
jobs <- n
}
close(jobs)
for i := 0; i < 5; i++ {
fmt.Println(<-results)
}
}引数の型に付いた <-chan と chan<- は、受信専用と送信専用の指定です。方向を絞っておくと、誤った向きの操作をコンパイル時に止められます。
つまずきやすいところ
close(jobs) を忘れると range が終わらず、全員が受信待ちのまま停止して deadlock で落ちます。逆に、閉じたチャンネルへ送信するとパニックになるため、閉じるのは送る側の責任とする決まりにしておくと事故が減ります。バッファ無しのチャンネルは送信側も受信側が受け取るまで止まるので、同じ goroutine の中で送って受け取ろうとすると、その場で止まります。