Goクイズアドベントカレンダー13日目

December 13, 2020

この記事は、Goクイズ Advent Calendar 2020の13日目の記事です。

問題

以下のコードを実行するとどうなるか?

package main
func init() {
	defer func() { recover() }()
	var ch chan struct{}
	done := make(chan <-chan struct{}, 1)
	done <- ch
	close(done)
	go func() { defer print("A"); close(ch) }()
	go func() { defer print("B"); <-ch; close(done) }()
	<-<-done
	print("C")
}
  1. コンパイルエラー
  2. ABCと表示される
  3. ABと表示される
  4. パニック

解答と解説

解答

答えは「1. コンパイルエラー」でした!

この問題では、以下のようなチャネルやゴルーチンの挙動について問われています。

  • チャネルのチャネルは定義可能か?
  • バッファありチャネルへの送信時の挙動
  • 閉じられたチャネルを再度閉じることができるのか?
  • 単方向チャネル型に全方向チャネル型は代入可能か?
  • チャネルのゼロ値
  • チャネル型のnilを閉じることが可能か?
  • 閉じられたチャネルからの受信が可能か?
  • 他のゴルーチンで発生したパニックはリカバー可能か?
  • デッドロックはリカバー可能か?

それでは1つずつ解説していきましょう。

チャネルのチャネルは定義可能か?

チャネルはいわゆるファーストクラスオブジェクトです。つまり、数値や文字列、関数などと同様に代入や引数、戻り値に使えます。当然ながらチャネルでやり取りするデータ型としても使えるため、チャネルのチャネルは定義可能です。

なお、チャネルのチャネルは、ゴルーチン間でコールバックをお願いする場合に使えます。

バッファありチャネルへの送信時の挙動

make関数の第2引数に1以上の値を指定するとチャネルにバッファを設けれます。 そして、バッファありチャネルへの値を送信する場合、バッファに空きがあれば処理はブロックされません。

閉じられたチャネルを再度閉じることができるのか?

1度閉じたチャネルを再度閉じるとパニックが発生します。 チャネルは送信側によって閉じることが前提であるため、 不特定多数によって閉じられる危険性は少ないでしょう。 しかし、フローが複雑な場合は誤って複数回閉じてしまうことが考えられます。 そのような場合には、sync.Onceを使って閉じておくとパニックを回避できます。

単方向チャネル型に全方向チャネル型は代入可能か?

チャネル型のうち、chan struct{}のような型を全方向チャネルと呼び、 chan<- struct{}<-chan struct{}のような型を単方向チャネルと呼びます。 chanキーワードの後ろに<-を記述すると送信専用、前に記述すると受信専用の単方向チャネルとなります。

単方向チャネル型の変数には、全方向チャネル型の値を代入できます。 ただし、チャネルで送受信する型は同じである必要があります。 なお、逆に全方向チャネル型の変数に単方向チャネル型の値を代入することはできません。

チャネルのゼロ値

チャネルのゼロ値はnilです。

チャネル型のnilを閉じることが可能か?

チャネル型のnilclose関数で閉じようとするとパニックが発生します。

閉じられたチャネルからの受信が可能か?

閉じられたチャネルからの受信は可能です。 常にチャネルでやりとりする型のゼロ値とfalseが取得できます。

なお、閉じられたチャネルに値を送信するとパニックが発生します。

他のゴルーチンで発生したパニックはリカバー可能か?

他のゴルーチンで発生したパニックはリカバーすることはできません。 そのため、パニックが発生しうる関数では必ずdefer文とrecover関数を使ってリカバーをする必要があります。

net/httpパッケージでは、リクエストごとにゴルーチンを作成しています。しかし、リクエストを処理するハンドラ内でパニックが発生した場合でもプロセスが終了しないように、リカバーする処理が設定されています。ただし、ハンドラ内でゴルーチンを生成し、その中でパニックが発生した場合はリカバーできないため、注意が必要です。

デッドロックはリカバー可能か?

デッドロックとは、すべてのゴルーチンがチャネルの送受信待ちなどのブロック状態にあることを指します。パニックのようにプロセスが終了してしまいますが、デッドロックはパニックではないため、リカバーすることは不可能です。

なお、言語仕様にはデッドロックについての記載がないため、実装依存になる可能性もあります。

では、なぜコンパイルエラーになるのか?

ここまでの解説を読んだ方は答えがなぜコンパイルエラーになるのか疑問を持っているかもしれません。 パニックが起きるのではないか?そう考えても仕方がないでしょう。

make(chan <-chan struct{}, 1)と記述してある部分ですが、実はフォーマットをかけるとmake(chan<- chan struct{}, 1)となります。つまり、受信可能な単方向チャネルを送受信可能な全方向チャネルを作成しているのではなく、送受信可能な全方向チャネルを送信可能な単方向チャネルを定義しています。

そのため、<-<-doneの部分で受信不可なチャネルから受信をしているため、コンパイルエラーとなります。

ちなみに、main関数ではなく、init関数で記述していることに深い意味はありません。Go1のリリースノートを見ると、Go1以前はinit関数ではゴルーチンが使えなかったようです。そう考えると、便利になったなと感慨深いものがあります。