終了したことを他の複数のゴルーチンに伝えるのにチャネルのcloseを使う #golang


複数のゴルーチンに対して、何かしらの処理が終了したことを伝えるのにチャネルの`close`が使えます。
これはruiさんの以下の記事にも書かれていることですが、ここでは閉じれたチャネルの挙動を見つつ、例を挙げて説明していきます。

* [Goでチャネルにブロードキャストするにはcloseを使う](http://qiita.com/ruiu/items/c58c343b589becc20937)

## 閉じられたチャネルの挙動

閉じられたチャネルは、受信したり送信したりした場合、どのような挙動をするのでしょうか？
実際に送信や受信を行ってみて挙動を確かめてみましょう。

### 送信

閉じられたチャネルに対して送信してみましょう。

[Go Playgroundで見る](https://play.golang.org/p/S8RKHw0zHx)

```
package main

func main() {
	ch := make(chan struct{})
	close(ch)
	ch <- struct{}{}
}
```

上記のコードを動かしてみると、以下のようにパニックが発生します。

```
panic: send on closed channel

goroutine 1 [running]:
panic(0x9b620, 0x1040a040)
	/usr/local/go/src/runtime/panic.go:500 +0x720
main.main()
	/tmp/sandbox279744884/main.go:6 +0x80
```

`panic`が発生していることからわかるように、基本的には閉じられているチャネルに対して送信することはしません。
そのため、チャネルが閉じられていることを送信する側がきちんと把握している必要があります。
しかし、どうしてもそうでない場合は`recover`してやると、`panic`を握りつぶすことができます。
この方法を使う場合は、他のパニックも握りつぶしてしまわないように気をつけましょう。

[Go Playgroundで見る](https://play.golang.org/p/W2PRCqw9L2)

```
package main

import "fmt"

func sendSafety(ch chan struct{}, v struct{}) (sent bool) {
	defer func() {
		if r := recover(); r != nil {
			return
		}
	}()

	ch <- v

	return true
}

func main() {
	ch := make(chan struct{})
	close(ch)
	fmt.Println(sendSafety(ch, struct{}{}))
}
```

動かしてみると、送れはしませんが`panic`も起きません。

```
false
```

### 受信

閉じられたチャネルから受信してみましょう。

[Go Playgroundで見る](https://play.golang.org/p/xPViThbx7w)

```
package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	fmt.Println("done", <-ch)
}
```

実行すると、受信しているように見えます。
しかし、どこから送られてくるのでしょうか？

```
done {}
```

実は、閉じられたチャネルから受信しようとすると即座にゼロ値が返ってきます。
上記の場合は、`struct{}`型のゼロ値が返ってきています。

また、カンマok記法を用いると、第2戻り値にチャネルが閉じられているかどうかが返ってきます。

[Go Playgroundで見る](https://play.golang.org/p/JgwqQrc8d9)

```
package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	v, ok := <-ch
	fmt.Println("done", v, ok)
}
```

*実行結果*

```
done {} false
```

`select`の`case`で受信しても即座にその`case`が実行されます。

[Go Playgroundで見る](https://play.golang.org/p/VwGOxROZPK)

```
package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	select {
		case <-ch:
			fmt.Println("done")
		default:
			fmt.Println("default")
	}
}
```

*実行結果*

```
done
```

ちなみに、閉じるのではなく、`nil`を入れておくと、その`case`は実行されません。
これは[nilチャネル](https://talks.golang.org/2013/advconc.slide#29)というイディオムなので、覚えておくと良いと思います。

[Go Playgroundで見る](https://play.golang.org/p/1TdjkkckY7)

```
package main

import "fmt"

func main() {
	ch := make(chan struct{})
	ch = nil
	select {
	case v := <-ch:
		fmt.Println("receive", v)
	case ch <- struct{}{}:
		fmt.Println("send")
	default:
		fmt.Println("default")
	}
}
```

*実行結果*

```
default
```

`range`で回すとどうなるでしょうか？

[Go Playgroundで見る](https://play.golang.org/p/6ylVE-p6d6)

```
package main

import "fmt"

func main() {
	ch := make(chan struct{})
	close(ch)
	for v := range ch {
		fmt.Println(v)
	}
	fmt.Println("done")
}
```

*実行結果*

```
done
```

実行すると、ループが回ってないことがわかります。
つまり、チャネルに対して`range`でループを回した場合に、ループを止めるにはチャネルを閉じるしかないということです。

### 複数回閉じる

すでに閉じられているチャネルをもう一度閉じてみましょう。

[Go Playgroundで見る](https://play.golang.org/p/TuPNpvlqOk)

```
package main

func main() {
	ch := make(chan struct{})
	close(ch)
	close(ch)
}
```

`panic`が発生し、閉じれないことがわかります。

```
panic: close of closed channel

goroutine 1 [running]:
panic(0x9b620, 0x1040a040)
	/usr/local/go/src/runtime/panic.go:500 +0x720
main.main()
	/tmp/sandbox820185450/main.go:6 +0x80
```

必ず1度しか閉じないようにするためにはどうすればよいでしょうか？
標準パッケージの`sync`パッケージに、1度だけ実行する際に使う便利な型があるので、そちらを使いましょう。

[Go Playgroundで見る](https://play.golang.org/p/43hCBrhErp)

```
package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan struct{})
	var once sync.Once

	for i := 0; i < 10; i++ {
		once.Do(func() {
			close(ch)
			fmt.Println("close")
		})
	}
}
```

*実行結果*

```
close
```

`sync.Once`は、`Do`メソッドでの関数呼び出しを1度だけ行います。
これは`sync`パッケージの型ですので、複数のゴルーチンから呼び出しても問題ありません。

[Go Playgroundで見る](https://play.golang.org/p/I4-B7nVRsF)

```
package main

import (
	"fmt"
	"sync"
)

func main() {
	ch := make(chan struct{})
	var once sync.Once

	var wg sync.WaitGroup

	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			once.Do(func() {
				close(ch)
				fmt.Println("close")
			})

			wg.Done()
		}()
	}

	wg.Wait()
}
```

## closeでブロードキャストしよう

前置きが長くなりましたが、処理が終了したことを複数のゴルーチンへブロードキャストしてみましょう。
前述の通り、閉じられたチャネルからは必ずゼロ値が送られてきます。
また、チャネルはバッファに何も値が入ってない場合には、受信時に処理をブロックします。
そのため、チャネルが閉じられるまでは、受信していると処理をブロックできます。
これらを利用すると、処理が終わるまでチャネルの受信で処理をブロックしておき、処理が終わるとチャネルがクローズされ、それが複数のゴルーチンに伝わるということが実現します。

それでは、具体的なコードを見てみましょう。

[Go Playgroundで見る](https://play.golang.org/p/TiRUj9axCK)

```
package main

import (
	"fmt"
	"sync"
)

func main() {

	var wg sync.WaitGroup

	done := make(chan struct{})

	for i := 0; i < 10; i++ {
		i := i
		wg.Add(1)
		go func() {
			<-done
			fmt.Println("done", i)
			wg.Done()
		}()
	}

	close(done)
	wg.Wait()
}
```

`for`文の中で`done`チャネルを受信していることがわかります。
各ゴルーチンの処理は一度ここでブロックされます。
そして、`close(done)`が呼び出された時点で、`<-done`からゼロ値が返されます。
そのまま各ゴルーチンの処理が再開され、画面に`"done　N"`(Nは数値)が表示されます。
なお、各ゴルーチンがどのような順序で実行されるかは決まっていないので、表示される順番はバラバラです。

*実行結果*

```
done 9
done 0
done 1
done 2
done 3
done 4
done 5
done 6
done 7
done 8
```

ここで注目したいのは、`close`を1回だけ呼び出しているところです。
通常どおり、終了したことを`done <- struct{}{}`のように送ると、待機しているゴルーチンの数だけ送る必要があります。
そうすると、この処理を待っているゴルーチンの数がわからない場合などうまく使えません。

この方法は、標準パッケージにも使われており、`context`パッケージで見ることができます。

* https://github.com/golang/net/blob/6250b412798208e6c90b03b7c4f226de5aa299e2/context/pre_go17.go#L182

## まとめ

閉じられたチャネルの挙動について説明し、チャネルを閉じることで複数のゴルーチンに処理が終了したことをブロードキャストする方法について説明しました。