Menu

Go言語のチャネル(channel):バッファ付き、バッファなし、closeとrange

Goのチャネルがゴルーチン間で値を受け渡す仕組みを解説します。バッファなしとバッファ付きのチャネル、クローズとrange、方向付きの型、デッドロックのエラー、そしてチャネルで作るパイプラインまで。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

送信と受信

チャネルは、ゴルーチン間をつなぐ型付きのパイプです。ch <- v で送信し、<-ch で受信します。make で作ります。

チャネル型のゼロ値は nil なので、make なしの var ch chan string は永遠にブロックするチャネルになります。チャネルは必ず make で作りましょう。

バッファなしチャネルは同期する

make(chan T) はバッファなしチャネルを作ります。送信は受信側が値を受け取るまでブロックし、受信は送信側が値を渡すまでブロックします。2つのゴルーチンはその時点で出会うので、バッファなしチャネルはデータのパイプであると同時に同期の道具でもあります。以下で <-done が戻ったとき、ワーカーが送信より前のすべてを終えたことがわかります。

ここでは main でミューテックスなしに result を読んでも安全です。Goのメモリモデルは、送信前にワーカーがしたことすべてが、対応する受信の後の main から見えることを保証しています。純粋な合図には chan struct{} を使うのがGoらしい書き方です。struct{} はメモリを使わないからです。

バッファ付きチャネル

make(chan T, n) はチャネルに n 個の値の余地を与えます。送信はバッファがいっぱいになるまで受信側なしで成功し、受信は空になるまで成功します。値は入れた順に出てきます。

バッファは送信側と受信側を切り離し、短い集中で送信側が止まらないようにします。消費者より恒常的に速い生産者を直すものではなく、送信側がブロックする瞬間を遅らせるだけです。バッファのサイズは、デッドロックを消すためではなく、理由(送信者の数、既知のバッチサイズ)をもって選びましょう。

len(ch) はその瞬間の値にすぎません。それに基づいて行動するときには、別のゴルーチンが変えているかもしれないので、送信がブロックするかどうかの判断には使わないでください。その用途には default ケース付きの select を使います。

closeとrange

close(ch) は、もう値が送られないことを受信側に伝えます。クローズの後は次のようになります。

  • すでにバッファにある値はそのまま届けられる
  • その後はすべての受信がすぐにゼロ値を返す
  • v, ok := <-chok == false を報告する
  • for v := range ch が終わる

close の後の最初の受信は、まだ ok == true"last" を受け取ります。2回目はゼロ値の ""false を受け取ります。

panicを引き起こすルールは次のとおりです。

  • 閉じたチャネルへの送信は send on closed channel でpanicする
  • すでに閉じたチャネルを閉じるとpanicする
  • nil のチャネルを閉じるとpanicする

ですから、閉じるのは送信側だけで、一度だけです。送信者が複数いる場合、他の送信者がいつ終わるかをどの送信者も知りません。別のゴルーチンにすべての送信者を(sync.WaitGroup で)待たせ、Wait が戻った後でチャネルを閉じます。クローズが必要なのは、受信側が終わりを待つときだけです。誰も参照していない閉じていないチャネルは、他の値と同じようにガベージコレクションされます。

方向付きの型

関数は、チャネルに対して送信だけ、または受信だけをすると宣言できます。するとコンパイラはもう一方の操作を拒否します。

意味許される操作
chan T双方向送信、受信、クローズ
chan<- T送信専用送信、クローズ
<-chan T受信専用受信

chan T は、関数に渡すときにどちらの制限付きの型にも暗黙に変換されます。上の produce<-chan int を返せるのはこのためで、呼び出し側はrangeできますが、送信もクローズもできません。当てはまる関数の引数には必ず方向付きの型を使いましょう。所有権を示し、誤用をコンパイルエラーに変えてくれます。

デッドロック

すべてのゴルーチンがブロックし、どれも起こせるものがなければ、ランタイムはプログラムを中断します。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/main.go:7 +0x38
exit status 2

ゴルーチンのダンプを見れば、どの操作が止まっているか(ここでは chan send、他に chan receivesync.WaitGroup.Waitselect)がわかります。よくある原因は次のとおりです。

  • 受信するゴルーチンが動いていないのに、バッファなしチャネルに送信している
  • 決して閉じられないチャネルに対して range している
  • WaitGroup のカウンタがゼロにならない
  • 2つのゴルーチンが互いを待っている

ランタイムが検出するのは、すべての ゴルーチンが止まっている場合だけです。他のゴルーチン(HTTPのリスナー、ティッカー)が生きているサーバーでは、同じバグでも何もクラッシュせず、ブロックしたゴルーチンがリークするだけです。

nilチャネル

nil のチャネルへの送信と受信は永遠にブロックします。役に立たないように聞こえますが、select の中では定番のテクニックです。チャネルの変数を nil にすると、そのケースが無効になります。次のコードは2つのチャネルを合流させ、それぞれが閉じたらもう聞かないようにします。

nil の代入がないと、閉じたチャネルは常に準備ができているので、ループはゼロ値で空回りします。

パイプライン

チャネルを組み合わせるとパイプラインになります。各段は1つのチャネルから受信して次のチャネルに送信するゴルーチンで、入力が終わったら自分の出力を閉じます。

各段は並行に動き、どの段も順序を保つ1つのゴルーチンなので、出力の順序は決まっています(1、16、81)。クローズは連鎖します。generate が閉じると square のrangeが終わり、それがその出力を閉じ、というように main まで伝わります。

このパイプラインの弱点は、main が途中で読むのをやめると、各段が送信で永遠にブロックすることです。実際のパイプラインは context.Contextdone チャネルを受け取り、すべての送信と並べてそれを select します。

チャネルかミューテックスか

チャネルは、データの所有権を渡すことと、出来事を知らせることのためのものです。キャッシュやカウンタのように、多くのゴルーチンがその場で読み書きする共有状態を守るには、sync.Mutexのほうが単純です。状態を所有してチャネル越しにリクエストに応えるゴルーチンより、ミューテックスを持つ構造体のほうが明快なことはよくあります。コードが短くなり、所有権がはっきりするほうを使いましょう。

早見表

操作nilチャネル開いたチャネル閉じたチャネル
ch <- v永遠にブロック受信されるかバッファに空きができるまでブロックpanic
<-ch永遠にブロック値が得られるまでブロックバッファの値、その後ゼロ値
v, ok := <-ch永遠にブロックok はtrue空になると ok はfalse
close(ch)panic閉じるpanic
len(ch)cap(ch)0、0バッファ内の値の数、バッファのサイズ残っている値の数、バッファのサイズ

よくある質問

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

バッファなしチャネル(make(chan int))は保存領域を持ちません。送信は別のゴルーチンが受信するまでブロックするので、どの送信も受け渡しであり同期点です。バッファ付きチャネル(make(chan int, 3))は最大3個の値を保持し、送信はバッファがいっぱいのときだけ、受信は空のときだけブロックします。

Goで閉じたチャネルから読むとどうなりますか?

閉じたチャネルからの受信は決してブロックしません。まずバッファに残っている値を取り出し、その後は要素の型のゼロ値を永遠に返します。区別するには v, ok := <-ch を使います。チャネルが閉じて空になると okfalse になります。for v := range ch のループはそこで止まります。

Goでチャネルを閉じるのは誰ですか?

送信側です。そして、受信側がもう値が来ないことを知る必要があるとき(たとえば range ループを終わらせるため)だけです。閉じたチャネルへの送信はpanicし、チャネルを2回閉じてもpanicするので、受信側がチャネルを閉じると送信側と競合します。チャネルを解放するために閉じる必要はありません。到達できないチャネルは、どちらにしてもガベージコレクタが回収します。

「fatal error: all goroutines are asleep」とはどういう意味ですか?

プログラム内のすべてのゴルーチンが、決して完了しえないチャネル操作やロックでブロックしているので、ランタイムがプログラムを止めたということです。最もよくある原因は、他に受信するゴルーチンがないのに main でバッファなしチャネルに送信することや、決して閉じられないチャネルに対してrangeすることです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める