【Go入門】Goの並行処理(2)_チャネルのクローズと範囲節によるループ

シェアする

こんにちは。Go入門ブログの第18回です。

前回の記事では、Goにおける並行処理機能であるゴルーチン(goroutine)チャネルの基本知識について解説しました。

【Go入門】Goの並行処理(1)_ゴルーチンとチャネル

今回も引き続き、チャネルの使用方法を更に掘り下げて解説していきます。

チャネルのクローズ

close関数を使用することで、チャネルを明示的にクローズすることができます。
チャネルをクローズすると、そのチャネルに対してデータを送信できなくなり、あまったバッファにはデータ型に対応するゼロ値がセットされます。

package main

import "fmt"

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	// チャネルのクローズ
	close(ch)
	x, y, z := <-ch, <-ch, <-ch
	fmt.Println(x, y, z)
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
1 2 0

なお、クローズされたチャネルにデータを送信した場合はランタイムパニックが発生します。

package main

import "fmt"

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	// チャネルのクローズ
	close(ch)
	// クローズ後にチャネルへデータ送信
	ch <- 3
	x, y, z := <-ch, <-ch, <-ch
	fmt.Println(x, y, z)
}
panic: send on closed channel

goroutine 1 [running]:
main.main()
/home/user/main.go:11 +0xb0
exit status 2

注意点として、データを受信する側のチャネルは、クローズするべきではありません。
クローズされたチャネルに対してデータを送信するとランタイムパニックが発生するためです。
逆に、クローズされたチャネルからデータを受信するのは問題ありません。
したがって、明示的なクローズを行うのは送信側のチャネルだけにするべきです。

また、チャネルは、クローズしなくてもガベージコレクタに回収されます。したがって、通常は明示的にクローズする必要はありません。
後述するrangeループを終了する場合など、これ以上値が来ないことを受け手が知る必要がある場合にクローズを行いましょう。

クローズ状態の確認

チャネルがクローズされているかどうかを確認するためには、受信の式に2つ目のパラメータを割り当てます。

v, ok := <-ch

このように記述すると、変数vにはチャネルから送信された値が、変数okにはチャネルのオープン状態を示す真偽値が代入されます。
受信する値がなく、かつチャネルが閉じているなら、 okの変数値はfalseになります。

実際に動作を確認してみましょう。

package main

import "fmt"

func main() {
	ch := make(chan int, 3)

	// チャネルに1つめのデータを送信
	ch <- 1

	// 1つめのデータ受信とクローズ状態確認
	x, x_ok := <-ch
	fmt.Println(x, x_ok)

	// チャネルに2つめのデータを送信してクローズ
	ch <- 2
	close(ch)

	// 2つめのデータ受信とクローズ状態確認
	y, y_ok := <-ch
	fmt.Println(y, y_ok)
	// 3つめのデータ受信とクローズ状態確認
	z, z_ok := <-ch
	fmt.Println(z, z_ok)
}

このサンプルの実行結果は以下のようになります。

1 true
2 true
0 false

2回目の出力では、既にチャネルはクローズしていますが、受信する値が存在するため、真偽値はtrueであることに注目してください。
3回目の出力で受信する値が存在しなくなり、真偽値がfalseに変わっています。

チャネルと範囲節によるforループ

チャネルに対しても、配列などと同様に、範囲節を使用したforループを行うことが可能です。
範囲節を使用したループでは、チャネルがクローズされるまで、そのチャネルから値を繰り返し受信し続けます。

しかし、この文法では、チャネルがクローズされているかどうかを受信側で検出できません。
したがって、以下のような書き方をすると、チャネルの範囲を越えてループが実行され、受信エラーが発生します。

package main

import "fmt"

func main() {
	ch := make(chan int, 3)
	ch <- 1
	ch <- 2
	ch <- 3
	for i := range ch {
		fmt.Println(i)
	}
}

このサンプルの実行結果は以下の通りになります。

$ go run main.go
1
2
3
fatal error: all goroutines are asleep – deadlock!

goroutine 1 [chan receive]:
main.main()
/home/user/main.go:11 +0x11c
exit status 2

このようなエラーを回避するにはどうすればよいでしょうか。
たとえば以下のように、並行処理でチャネルのクローズを行うような使い方が考えられます。

package main

import (
	"fmt"
)

// カウントアップ関数
func countup(n int, c chan int) {
	// 1からチャネルのバッファ数だけループ
	for i := 1; i <= n; i++ {
		// チャネルにiを送信
		c <- i
	}
	// チャネルのクローズ
	close(c)
}

func main() {
	// バッファ数5のチャネルを生成
	c := make(chan int, 5)
	// ゴルーチンを生成しカウントアップ関数を実行
	go countup(cap(c), c)
	// チャネルがクローズするまでループ
	for i := range c {
		fmt.Println(i)
	}
}

このサンプルの実行結果は以下の通りになります。

$ go run main.go
1
2
3
4
5

先ほどのようなエラーが発生せず、意図するとおりにチャネルの全データを受信できたことが確認できます。

終わりに

本記事では、前回の記事に引き続いてチャネルの利用方法を解説しました。
並行処理に関する解説は次回の記事でひと段落となります。

並行処理を必要としないプログラミングでは使用しない知識ではありますが、Goの大きな特徴ともいえる機能ですので、引き続きお付きあいください。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク