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

# 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 せず、読み取れる。

#### 読むべき値がない場合

```go
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 前に値を積んである場合

```go
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 の場合は止まらずに「読めなかったね、じゃあ次」となるので問題ない。

```go
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 は、このコードでは読まれない。

```go
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 にわたすと、そこで処理が止まる。

```go
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 したのと同じような状況になる。

```go
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|
|:--|:--|:--|
|読み出し|そこで停止|ゼロ値が得られる|
|書き込み|そこで停止|パニック|
| `select` 〜 `case` |その case にはならない|ゼロ値が得られる|
|`range`|そこで停止|ループに内入らずにループ終了|
|close|panic|panic|

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

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

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

こんなの。

```go
type hoge struct {
	ch chan struct{}
}

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

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

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

こんなの。

```go
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

こんなの。

```go
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.ch` が `nil` → なにもしない

ということになる。
read の例と同じく、[B] の時点では `h.ch` は `nil` かもしれないので、 `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 済みだということを知ることができる。