LoginSignup
5
2

More than 3 years have passed since last update.

go の chan のことをよく知らなかったので試してみた

Last updated at Posted at 2021-02-21

go の chan のことをよく知らなかったので試してみた

知っておくといいかもしれないこと。

go の chan が close済みかどうかを知る方法は、昔あったらしいけど今はない。
途中のバージョンで削除されたらしい。

削除された理由はおそらく、無いほうがいいから。あるとむしろトラブルの原因になる。
「close済みでなければ xxx する」というコードを書いても、「close済みでなければ」と「xxxする」の間で他人が close する可能性があり、それを排除することができない。

chan が nil だったり close済みだったりする場合の動作

nil の chan からの読み出しと書き込み。

そこで止まり、書き込み・読み出しが終わることはない。
それが唯一の goルーチンの場合、panic する。Recover できない。

close済み の chan への書き込み

panic する。Recover できる。
send on closed channel というメッセージ。
型は runtime.plainError

close済み の chan からの読み出し

panic せず、読み取れる。

読むべき値がない場合

ch := make(chan [3]int)
close(ch)
log.Println(<-ch, "ok") //=> [0 0 0] ok
log.Println(<-ch, "ok") //=> [0 0 0] ok
log.Println(<-ch, "ok") //=> [0 0 0] ok

ゼロ値が取れる。

close 前に値を積んである場合

ch := make(chan int, 2)
ch <- 111
ch <- 222
close(ch)
log.Println(<-ch, "ok") //=> 111 ok
log.Println(<-ch, "ok") //=> 222 ok
log.Println(<-ch, "ok") //=> 0 ok
log.Println(<-ch, "ok") //=> 0 ok

close 前に積んだ値が読める。
積んだ値がなくなったらゼロ値になる。

nil の chan を含む select

nil である chan から読み出しを試みるとそこで止まってしまうが、 select の場合は止まらずに「読めなかったね、じゃあ次」となるので問題ない。

chI := make(chan int)
var chF chan float64 = nil
go func() {
    time.Sleep(500 * time.Millisecond)
    chI <- 123
}()
select {
case i := <-chI:
    log.Println("i:", i) // ここに来る。
case f := <-chF:
    log.Println("f", f) // ここには来ない。
}

上記の例では、500ms 後に chI から 123 が読み出され、特に困ったことは何も起きない。

close 済み chan を含む select

close 済みの chan から読み込むとゼロ値が取れるので、以下の例では即座に close 済みの chF から 0.0 が読まれる。
500ms 後に chI に積まれる 123 は、このコードでは読まれない。

chI := make(chan int)
chF := make(chan float64)
close(chF)
go func() {
    time.Sleep(500 * time.Millisecond)
    chI <- 123
}()
select {
case i := <-chI:
    log.Println("i:", i) // ここには来ない。※
case f := <-chF:
    log.Println("f", f) // ここに来る。
}

※ select を含む go ルーチンが何らかの事情で goルーチン作成から select 到達までに 500ms 以上の時間を要していたら chI の方に来るかもしれない。

nil である chan の range

nil である chan を range にわたすと、そこで処理が止まる。

var ch chan int = nil
for i := range ch {
    log.Println(i)
}
log.Println("exit for")

ループ内に入ることもないし、ループを抜けることもない。
このループが唯一の go ルーチンである場合、recover 不能な panic が発生する。

close 済み chan の range

当たり前だけど、 for..range のループが始まってから close したのと同じような状況になる。

ch := make(chan int)
close(ch)
for i := range ch {
    log.Println(i) // ここには来ない
}
log.Println("exit for") // すぐにここに来る

つまり、ループには入らず、すぐにループを抜ける。

nil である chan と close 済み chan の close

両方とも panic になる。
型は runtime.plainError
メッセージは「close of nil channel」と「close of closed channel」。

まとめ

やること nil である chan close 済み chan
読み出し そこで停止 ゼロ値が得られる
書き込み そこで停止 パニック
selectcase その case にはならない ゼロ値が得られる
range そこで停止 ループに内入らずにループ終了
close panic panic

こうしてみると、 nil である chan は、ほぼ、書き込み側からは「誰も読んでくれない バッファのない chan」、読み出し側からは「誰も書いてくれない chan」として機能しているが、 close の振る舞いだけが異なる。

chan の nil チェックを含むパターン

フィールドの nil チェック後に read / write

こんなの。

type hoge struct {
    ch chan struct{}
}

func (h *hoge) foo() {
    if h.ch == nil {
        return
    }
    // [A]
    a := <-h.ch // [B]
    doSomething(a)
}

これはダメ。 [A] の箇所で誰かが h.chnil にするかもしれない。nil になると [B] で止まる。
上記の例は read だけど、write も同じ理由でダメ。

ローカル変数の nil チェック後に read

こんなの。

func (h *hoge) bar() {
    ch := h.ch
    if ch == nil {
        return
    }
    // [A]
    a := <-ch // [B]
    doSomething(a)
}

これは、明らかに駄目ということではないが、わりとよろしくない感じ。
[A] で h.ch を誰かが nil にしても、 ch にはその前の値が入っている。
ただ

  • ch はすでに close 済みで、[B] で読める値はゼロ値かもしれない。

ということで、わりとまずいことになりそうではある。

ローカル変数の nil チェック後に write

こんなの。

func (h *hoge) baz() {
    ch := h.ch
    if ch == nil {
        return
    }
    ch <- struct{}{} // [B]
}

[B] がnil への write にならないことは間違いないが、 ch が close 済みだとパニックになる。

つまり、事前条件と結果は

  • h.ch が close 済みではない → ch になんか送る
  • h.ch が close 済み → panic
  • h.chnil → なにもしない

ということになる。
read の例と同じく、[B] の時点では h.chnil かもしれないので、 ch を read する人はもういないかもしれない。

というわけで

というわけで、 chan が nil かどうかで動作を変えるのはわりとうまく行かない感じ。
「close したら nil にして、nil かどうかで条件分岐」とかいう作戦より「そもそも close かどうかが不確定な状況にならないようにする」という作戦を取るべきと思う。

read 側が range で回せば write 側の close でループ抜けるので、それで済むならそれがよい。
あるいは、一回しか write しなくて、書き込み側は write → close。読み出し側は read 一回とか。

chan の仕様がそのようなメッセージとなっていると思う。

複雑なケースの場合に安全なパターンを作るのが難しくなることもあると思うけど、 nil チェックでは安全にしにくい。

あと。close 済みの chan を read するとゼロ値が得られるのが気持ち悪いような便利なような。
たとえば、 chan bool にしておいて、いつも ch<-true としておけば、 false が得られた場合に close 済みだということを知ることができる。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2