Go言語 channel の使い方を勘違いしてたこと

「Go言語による並行処理」を写経してるんですが、チャネルのところで理解が甘かったところがありました。

やってみて、ああそうだったのか!?と気づきました。

わかってなかったことは下記の2つ

  • チャネルってgoroutineで使うもの。
  • そのgoroutineの中でcloseしても大丈夫(むしろこうするべき)

いやーいい加減に理解してはダメですね。

チャネルを閉じて値を取得してみる

サンプルコードを見てみましょう。「Go言語による並行処理」から抜粋してみます。

サンプル01
https://play.golang.org/p/AjTlu6R4K-p

package main

import "fmt"

func main() {
        intStream := make(chan int)
        close(intStream)
        integer, ok := <- intStream
        fmt.Printf("(%v): %v", ok, integer)
}

結果は下記になります

(false): 0

なにも値をいれてないチャネルから値を取り出しても大丈夫だよ、ということです。

main() でチャネルに値を入れてみる

ではここで intStream チャネルになにか値を入れてみましょう。
そのために close(intStream) もdefer にします。

サンプル02
https://play.golang.org/p/3EemcCFsELK

package main

import "fmt"

func main() {
	intStream := make(chan int)
	defer close(intStream)
	intStream <- 3
	integer, ok := <- intStream
	fmt.Printf("(%v): %v", ok, integer)
}

結果は下記になります。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/tmp/sandbox674957142/main.go:8 +0x80

え!?と思いましたがゴルーチン以外で値を入れるとデッドロックになるんですね!

ゴルーチンから値を入れてみる

ではここで intStream チャネルをgoroutine から値を入れてみます。

サンプル03
https://play.golang.org/p/ZZRzjnqnz2v

package main

import "fmt"

func main() {
	intStream := make(chan int)
	defer close(intStream)
	go func() { intStream <- 3 }()
	integer, ok := <- intStream
	fmt.Printf("(%v): %v", ok, integer)
}

結果です

(true): 3

うまくいきましたね。

ゴルーチンでチャネルを閉じてみる

ではここで defer close(intStream) を goroutine に移してみましょう。

予想として、goroutine の中でチャネルを閉じてしまったら受け取るときにエラーなるんじゃないか!?っと思ってました。
だってチャネルを閉じてるんだもん。

サンプル04
https://play.golang.org/p/BfrB7K1g8QR

package main

import "fmt"

func main() {
	intStream := make(chan int)
	go func() {
		defer close(intStream)
		intStream <- 3
	}()
	integer, ok := <- intStream
	fmt.Printf("(%v): %v", ok, integer)
}

結果です

(true): 3

エラーにならず正常な結果ですね!

待受け側 for ... range チャンネルは事前にチャンネルのcloseが必要

あともう一つチャンネルの close で気になった性質。

チャンネルに値を入れた後に、そのチャンネルから値を取得する場合って for ... range で値を取得する場合がありますよね。
下記のサンプルを見てみてください。

サンプル05
https://play.golang.org/p/aA0t3EVTCxn

package main

import "fmt"

func main() {
	// 値が入ったチャンネルを返す関数
	f := func() chan int{
		// チャンネル作って
		intStream := make(chan int)
		
		// ゴルーチンの中で値入れて
		go func() {
			defer close(intStream)
			intStream <- 3
		}()
		
		// チャンネルを返す
		return intStream
	}
	
	ch := f()
	// range で値を取得
	for integer := range ch {
		fmt.Printf("%v ", integer)
	}
}
3

結果はちゃんと取得できます。

この時ゴルーチンの中のチャンネルcloseしなかったらどうなるのかな?と思いました。

サンプル06
https://play.golang.org/p/PYvnH83M8Oe

package main

import "fmt"

func main() {
	// 値が入ったチャンネルを返す関数
	f := func() chan int{
		// チャンネル作って
		intStream := make(chan int)
		
		// ゴルーチンの中で値入れて
		go func() {
			//defer close(intStream)    <-- これな!!
			intStream <- 3
		}()
		
		// チャンネルを返す
		return intStream
	}
	
	ch := f()
	// range で値を取得
	for integer := range ch {
		fmt.Printf("%v ", integer)
	}
}

結果はこれでした

3 fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/tmp/sandbox416365219/main.go:23 +0x100

デッドロックエラーになるんですね。
考えてみればそりゃそうですよね。for で値取得をずーっと待つことになるんですから終わりきらないのでdeadlock。当たり前だ。

ではこの値取得を <- で取得したらどうなるのか?

サンプル07
https://play.golang.org/p/d9AjTBsc3Q6

package main

import "fmt"

func main() {
	// 値が入ったチャンネルを返す関数
	f := func() chan int{
		// チャンネル作って
		intStream := make(chan int)
		
		// ゴルーチンの中で値入れて
		go func() {
			//defer close(intStream)    <-- これな!!
			intStream <- 3
		}()
		
		// チャンネルを返す
		return intStream
	}
	
	ch := f()
	// <- で値を取得
	num := <- ch
	fmt.Printf("%v ", num)
}
3

値はちゃんと取れますね。<- で値を取得するだけであればチャンネルをcloseしなくても値取得できるんですね。
チャンネルをちゃんと閉じないのはあまりよろしくないですが。。。

チャネル所有するゴルーチンはチャネルを閉じる責任がある

「Go言語による並行処理」のP77では、ざっくりですが、チャネル所有するゴルーチンはチャネルを閉じる責任があるそうです

そうすることでチャネル所有者がチャネルを閉じるので、閉じたチャネルに書き込んでしまったり、2度チャネルを閉じてしまったり、といったpanicを引き起こすことがなくなります。

なるほどーチャネルってどこでも使えるのかと思ってましたが、そうでもないんですね。
勉強になりました!

Go言語による並行処理

Go言語による並行処理

  • 作者: Katherine Cox-Buday,山口能迪
  • 出版社/メーカー: オライリージャパン
  • 発売日: 2018/10/26
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログを見る