【小ネタ】Goで複数チャネルのCloseを待ち受ける

要約
Goでプログラムを作っていると、たまに複数チャネルのCloseを待ち受けるということがあります。 あまり馴染みがないのと、ググってもあまり出てこないのでメモ。
問題のコード
メンテをしているGoのプログラムで下記のコードがありました。
- channelが複数(ch1,ch2)あり、for selectで両方受信する
- ch1が先にcloseされる前提となっており、ch1のcloseではcontinueしている
- ch2がcloseされたらLoopを抜ける
package mainimport ( "log" "time")func main() { ch1 := make(chan struct{}) ch2 := make(chan struct{}) go func() { defer close(ch1) for i := 0; i < 2; i++ { ch1 <- struct{}{} time.Sleep(time.Second) } }() go func() { defer close(ch2) for i := 0; i < 5; i++ { ch2 <- struct{}{} time.Sleep(time.Second) } }()Loop: for { select { case _, ok := <-ch1: if !ok { // log.Println("ch1 is closed") continue } log.Println("ch1") case _, ok := <-ch2: if !ok { // log.Println("ch2 is closed") break Loop } log.Println("ch2") } }}
実行すると下記のように出力されます。
$ go run main.go 2021/03/18 22:57:01 ch22021/03/18 22:57:01 ch12021/03/18 22:57:02 ch12021/03/18 22:57:02 ch22021/03/18 22:57:03 ch22021/03/18 22:57:04 ch22021/03/18 22:57:05 ch2
問題ないように見えますが、ch1がcloseされたあとのログを出力すると2021/03/18 22:59:54 ch1 is closed
のようなログが大量に出力されることがわかります。
Receiving operatorの言語仕様を確認します。
A receive operation on a closed channel can always proceed immediately, yielding the element type's zero value after any previously sent values have been received.
出典: Receiving operator - The Go Programming Language Specification
閉じたチャネルの受信操作は、常にすぐZero値を返します。
改善する
ch1がcloseされたら、次の繰り返しではch1を受信するcase文を通らないでほしいですね。 channel closeされたらnil channelにしてブロックするようにします。
Receiving from a nil channel blocks forever.
出典: Receiving operator - The Go Programming Language Specification
Loop: for {+ if ch1 == nil && ch2 == nil {+ break Loop+ } select { case _, ok := <-ch1: if !ok {- // log.Println("ch1 is closed")+ log.Println("ch1 is closed")+ ch1 = nil continue } log.Println("ch1") case _, ok := <-ch2: if !ok {- // log.Println("ch2 is closed")- break Loop+ log.Println("ch2 is closed")+ ch2 = nil+ continue } log.Println("ch2") }
実行してみます。
$ go run main.go 2021/03/18 23:03:52 ch22021/03/18 23:03:52 ch12021/03/18 23:03:53 ch22021/03/18 23:03:53 ch12021/03/18 23:03:54 ch1 is closed2021/03/18 23:03:54 ch22021/03/18 23:03:55 ch22021/03/18 23:03:56 ch22021/03/18 23:03:57 ch2 is closed
これで正しく動きましたね。
まとめ
正しく動いていそうで、動いていないプログラムなどがあると厄介ですよね。 goroutineを使った並列処理などは特にデバッグが難しいので、できるだけ正しい知識を身に着けて実装・レビューしたいものです。
