Go

【Go】チャネルの基本的な使い方

前回はGoの並行処理の基本となるゴルーチン、チャネルという要素について基本的な使い方を説明しました。

前回の内容と被るところもありますが、今回はチャネルの使い方について説明します。

チャネルとは

ルーチン(関数)同士で値の送受信を行う為のものです。

チャネル変数の宣言

make関数を使用します。キーワード “chan” とチャネルで送受信する値の型を定義します。
宣言した変数はmake関数が使用されているので参照型(ポインタ)となる事に注意して下さい。

channelA := make(chan string)

チャネルへの読み書き

ルーチン(関数)同士がチャネルを通じて値の送受信を行う為、チャネルへ値の書き込み、読み込みが行えます。
値はチャネルを宣言した際に定義した型が使用できます。

チャネルの送受信の型がstringの場合 – 書き込み例

channel <- "sampleGoRoutineA"

チャネルの送受信の型がstringの場合 – 読み込み例

val := <-channel

チャネルの方向性

チャネルはルーチン(関数)同士が値を送受信する為のものなので、一つのルーチンでチャネルの読み書きを行うのは一般的ではありません。その為、関数の引数にチャネルが読み込み専用なのか、書き込み専用なのかを定義できます。

書き込み、読み込み専用として宣言しなくても動作しますが、宣言しておくことでGoがコンパイル時に読み込み専用に書き込みを行なっていないか、またはその逆をしていないかを検知してくれます。

// 書き込み専用
func sampleGoRoutineA(channel chan<- string) {
		channel <- "sampleGoRoutineA"
}

// 読み込み専用
func sampleGoRoutineB(channel <-chan string) {
		val := <-channel
}

チャネルのクローズ

前回の説明にはありませんでしたが、チャネルをクローズする事ができます。
特別な場合を除いて、書き込み側でチャネルへの書き込みが完了したら、チャネルをクローズするようです。

クローズの方法

close(チャネル名)でチャネルをクローズできます。

close(channel)

クローズされたかの判断

読み込み側はチャネルがクローズされるとブロックが解除され後続処理が実行されます。
チャネルから読み込める値は、書き込みされた値がない場合、チャネル宣言した変数の型のゼロ値が返却されます。
この為、チャネルがクローズされたかどうかを判断できません。(意図的なゼロ値なのかクローズされたゼロ値か分からない)
読み込み側はチャネルがクローズされたかどうかを判断するにはカンマOKイディオムを使用します。
2つ目の返り値がfalseの場合、チャネルがクローズされている事になります。一つ目の値にはチャネルから読み込んだ値を取得できます。

m, ok := <-channelA

使用例

この例では意図的にチャネルに値を書き込まずにチャネルをクローズし、クローズされた事を検知します。

package main

import (
	"fmt"
	"time"
)

func sampleGoRoutineA(channel chan<- string) {
	fmt.Printf("【開始】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	time.Sleep(5 * time.Second)
	fmt.Printf("【終了】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	//channel <- "終了A \n"
	close(channel)
}

func main() {
	channelA := make(chan string)
	go sampleGoRoutineA(channelA)

	m, ok := <-channelA
	if ok {
		fmt.Println(m)
	} else {
		fmt.Println("channel closed")
	}
}

結果は以下の通りです。

【開始】 19:26:52 sampleGoRoutineA 
【終了】 19:26:57 sampleGoRoutineA 
channel closed

チャネルの値取得とクローズ

先ほどのプログラムにて、ゴルーチン側を以下のように変更します。チャネルへ書き込みをチャネルのクローズの前に行うと、メインルーチンではクローズを待たずに処理が継続されてしまいます。あまり意味がないですね。

func sampleGoRoutineA(channel chan<- string) {
	fmt.Printf("【開始】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	time.Sleep(5 * time.Second)
	fmt.Printf("【終了】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	channel <- "終了A \n"
	close(channel)
}

チャネルのクローズも行いたい場合、For文やFor range文を使用します。

Forパターン

package main

import (
	"fmt"
	"time"
)

func sampleGoRoutineA(channel chan<- string) {
	fmt.Printf("【開始】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	time.Sleep(5 * time.Second)
	fmt.Printf("【終了】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	channel <- "終了A \n"
	close(channel)
}

func main() {
	channelA := make(chan string)
	go sampleGoRoutineA(channelA)

	m, ok := <-channelA
	for {
		m, ok := <-channelA
		if ok {
			fmt.Printf(m)
		} else {
			fmt.Printf("channel closed")
			break
		}
	}
}

結果は以下のようになります。

【開始】 19:38:03 sampleGoRoutineA 
【終了】 19:38:08 sampleGoRoutineA 
終了A 
channel closed

For rangeパターン

こちらの方がスマートかもしれません。

package main

import (
	"fmt"
	"time"
)

func sampleGoRoutineA(channel chan<- string) {
	fmt.Printf("【開始】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	time.Sleep(5 * time.Second)
	fmt.Printf("【終了】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	channel <- "終了A \n"
	close(channel)
}

func main() {
	channelA := make(chan string)
	go sampleGoRoutineA(channelA)

	for val := range channelA {
		fmt.Printf(val)
	}
}

結果は以下のようになります。

【開始】 19:41:03 sampleGoRoutineA 
【終了】 19:41:08 sampleGoRoutineA 
終了A 

チャネルクローズの必要性

チャネルへの書き込み、読み込みが終わればそれでよいのでクローズは必要ないのでは?と思いますが、クローズしないと永遠にチャネルを読み込む為、デットロックになる可能性があります。従って役目を終えたチャネルはクローズしましょう。
チャネルをクローズせずデットロックを起こしてしまうパターンを紹介します。

For range でのデットロック

For range はチャネルがクローズするまでチャネルの読み込みが行われます。その為クローズしないと読み込み待ちが続く事によるデットロックが発生します。

package main

import (
	"fmt"
	"time"
)

func sampleGoRoutineA(channel chan<- string) {
	fmt.Printf("【開始】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	time.Sleep(5 * time.Second)
	fmt.Printf("【終了】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	channel <- "ああああ"
}

func main() {
	channelA := make(chan string)
	go sampleGoRoutineA(channelA)

	for val := range channelA {
		fmt.Printf(val)
	}
}

以下のようになります。

【開始】 10:28:25 sampleGoRoutineA 
【終了】 10:28:30 sampleGoRoutineA 
ああああfatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
	/Users/hoge/main.go:46 +0xad
exit status 2

ゴルーチン側でクローズ処理を行います。

func sampleGoRoutineA(channel chan<- string) {
	fmt.Printf("【開始】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	time.Sleep(5 * time.Second)
	fmt.Printf("【終了】 " + time.Now().Format(time.TimeOnly) + " sampleGoRoutineA \n")
	channel <- "ああああ"
	close(channel)
}

以下のようにエラーが発生せず正常に実行されます。

【開始】 10:37:48 sampleGoRoutineA 
【終了】 10:37:53 sampleGoRoutineA 
ああああ

最後に

今回はチャネルの全てを説明していませんがチャネルについて説明しました。

コメントを残す

メールアドレスが公開されることはありません。

© DeNnie.Lab All Rights Reserved.