Menu

Go言語のselect文:タイムアウト、default、quitチャネル

selectが複数のチャネル操作を同時に待つ仕組みを解説します。準備のできたケースからの選択、defaultによるブロックしない送受信、time.Afterによるタイムアウト、そしてquitチャネルやcontextによるループの停止まで。

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

複数のチャネルを待つ

selectswitch に似ていますが、各ケースはチャネルの送信か受信です。どれかのケースが進められるようになるまでブロックし、それを実行します。

この遅延では、1回目の select が速い結果を受け取り、2回目が遅い結果を受け取ります。どちらの受信も、もう一方のチャネルを待つ必要はありません。単純に <-slow の後に <-fast と書くと、どちらが先に届いても決まった順序で処理することになります。

select の評価の仕方は次のとおりです。

  1. select が始まるときに、すべてのチャネルの式と送信する値が、ソースの順に一度だけ評価される。
  2. 1つ以上のケースの準備ができていれば、そのうち1つが ランダムに 選ばれる。
  3. どれも準備ができておらず default があれば、default が実行される。
  4. そうでなければ、どれかのケースの準備ができるまでゴルーチンがブロックする。

空の select {} は永遠にブロックします。本当の処理が他のゴルーチンで行われるプログラムの main の最後で、たまに見かけます。

準備のできたケースからのランダムな選択

複数のケースが同時に準備できているとき、select は最初に書かれたケースを優先しません。このプログラムは2つのバッファ付きチャネルを満たしてから、1000回selectします。

countAcountB の配分は実行のたびに変わり、それぞれ500前後になります。ランダムな選択は意図的なもので、忙しいチャネルが他を飢えさせるのを防ぎます。優先度が必要なら、後で紹介するパターンを参照してください。

defaultでブロックしない操作

default ケースがあると、select は決してブロックしません。これで送信や受信が「試す」操作になります。

バッファがいっぱいのときに処理を捨てるのは、呼び出し側を決して止めずに負荷を逃がしたりメトリクスを出したりする方法です。

チャネルを何度も「チェック」するためだけに、for ループ内の selectdefault を入れてはいけません。何も準備ができていないと、ループはCPU使用率100%で空回りします。代わりにブロックし、定期的に起きる必要があるならタイムアウトのケースを加えます。

タイムアウト

time.After(d) は、d の後に一度受信できるチャネルを返します。本当の処理と競わせます。

1回目の呼び出しは "data" を返します。2回目は、ワーカーが終わるよりずっと前の50ミリ秒後にタイムアウトのエラーを返します。結果のチャネルのバッファを1にしているのは意図的です。タイムアウトが勝つと、誰も result から受信しません。バッファなしのチャネルだと、ワーカーのゴルーチンは送信で永遠にブロックしてリークします。

ループの中の time.After は反復のたびに新しいタイマーを作ります。これは、メッセージごとのアイドルタイムアウト(「1秒間メッセージがない」)にはまさに正しい挙動です。多くの操作にまたがる全体の期限には、ループの前にタイマーかコンテキストを1つ作ります。Go 1.23以降、参照されなくなったタイマーは発火していなくてもガベージコレクションされるので、古いバージョンのようにループ内の time.After が各タイマーの発火までメモリを保持することはなくなりました(go.modgo 1.23 以降が必要です)。

for-selectループとquitチャネル

止めるよう言われるまで動くゴルーチンは、処理のケースと停止のケースを持つ selectfor ループで囲んだものです。

quit に送信するのではなく閉じるのがイディオムです。クローズは今も後もすべての受信側に見えるので、1回の close で何個のワーカーでも止められます。done チャネルで、main はワーカーが本当に戻るまで待てます。

実際のコードでは、quitチャネルはたいてい context.Context です:case <-ctx.Done():。同じように動き(Done() はキャンセル時に閉じられるチャネルを返します)、期限と停止の理由も運びます。contextのページで扱っています。

selectの中のbreak

select のケースの中の break は、外側の for ではなく select を抜けます。終わらないループのよくある原因です。return を使うか、ループにラベルを付けます。

チャネル間の優先度

select はランダムに選ぶので、1つの文の中でケースに順位を付けることはできません。あるチャネルに何かがあるときは常にそれを勝たせたいなら、先にそれだけをチェックします。

for {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}

	select {
	case <-ctx.Done():
		return ctx.Err()
	case job := <-jobs:
		handle(job)
	}
}

1つ目の select は、すでにキャンセルが起きていればすぐに戻ります。これがないと、ctx がキャンセルされた後もしばらく、途切れないジョブの流れがランダムな選択で勝ち続けることがあります。

nilチャネルでケースを無効にする

nil のチャネルへの送信や受信は決して準備ができないので、nil のチャネルのケースは実質的にオフになります。閉じた入力を nil にすれば、他のチャネルは続けたまま、それをselectするのをやめられます。この方法で作った合流のループはチャネルのページにあります。同じテクニックでタイムアウトのオンとオフも切り替えられます。必要になるまで var timeout <-chan time.Timenil にしておき、必要になったら time.After(d) を代入します。

よくある間違い

  • ソースの順序を期待する。 最初に書かれたケースが優先されるわけではありません。
  • default による空回りのループ。 することがないときの for { select { ... default: } } はCPUコアを1つ使い切ります。
  • 負けた側をリークさせる。 タイムアウトが勝ったときも、結果を送るはずだったゴルーチンは終われなければなりません。そのチャネルのバッファを1にします。
  • select だけを抜ける break ラベルか return を使います。
  • ループの反復ごとの time.After を全体の期限として使う。 反復のたびにやり直しになります。期限はループの外で一度だけ作ります。

よくある質問

Goのselectは何をしますか?

select は、チャネル操作(送信か受信)のどれかが進められるようになるまで待ち、そのケースを実行します。同時に複数が準備できていれば、ランダムに1つを選びます。default ケースがなければ、どれかのケースの準備ができるまでブロックします。default があれば決してブロックしません。

Goでチャネルの受信にタイムアウトを付けるには?

受信とタイマーを1つの select に入れます:select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }。先に起きたほうが勝ちます。ループの中や、呼び出し側がすでに期限を持っている場合は、代わりに context.WithTimeout で作った context.Context を使い、ctx.Done() をselectします。

Goのselectはケースを順番に選びますか?

選びません。複数のケースの準備ができているとき、Goは一様にランダムに選ぶので、どのケースも他を飢えさせることはありません。優先度が必要なら、まず優先度の高いチャネルを default 付きの単独の select でチェックし、それからすべてのチャネルに対する select に進みます。

なぜbreakでfor-selectのループを抜けられないのですか?

select の中の break は、外側の for ではなく select 文だけを抜けます。return を使うか、ループにラベルを付けます(loop: for { select { case <-done: break loop } })。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める