プログラミング
記事内に商品プロモーションを含む場合があります

Go言語のチャネルのまとめ|ゴルーチン間でデータのやり取りをする方法

tadanori

Go言語のチャネルについてまとめました。Goではゴルーチンとチャネルを利用することで簡単に並列実行させることが可能です。

Go言語のチャネルとは

Go言語のチャネル(channel)は、ゴルーチン(goroutine)間でデータを送受信するための機能です。

ゴルーチンは、Go言語の軽量スレッドで並列に動作します。チャネルは、スレッド間の安全な通信手段として使用します。

簡単に言えば、チャネルは、特定の型のデータを送受信するための通信パイプ(または、通信バッファ)のようなものになります。

ここでは、Goのチャネルの使い方について解説します。

channel(基本)

channelの生成

チャネルを生成するには、makeを使います。

チャネル名 := make(chan データ型)

   または、

チャネル名 := make(chan データ型, バッファサイズ)

例えば、整数型のチャネルを作成する場合は以下のようになります。

ch := make(chan int)

また、バッファサイズを指定する場合は以下のようになります。

ch := make(chan int, 3)

バッファサイズに関しては、後で詳しく説明します。

channelを用いたデータ送受信

チャネルにデータを送る

チャネルにデータを送る場合は、チャネル名<-データを使います。

チャネル名 <- データ

チャネルからデータを受け取る

チャネルからデータを受け取る場合は、変数<-チャネルになります。

変数 <- チャネル名

なお、チャネルを利用したデータの送受信を行うには、ゴルーチンが動いている必要があります。例えば以下のようなコードはエラーとなります。

package main

import "fmt"

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

	ch <- 101

	v := <-ch

	fmt.Println(v)
}
// fatal error: all goroutines are asleep - deadlock!

以下、goroutineとチャネルを利用してデータの送受信する例です。

package main

import "fmt"

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

	go func() {
		ch <- 100
	}()

	v := <-ch

	fmt.Println(v) // 100
}

上記の例ではインラインで関数を定義し、goでゴルーチンとして呼び出しています。

なお、チャネルからの受け取りでは2つの戻り値を受けることができます。2つ目の引数は、bool型で、受け取ったデータが送信されたデータかどうかをtrue/falseで返します。

package main

import "fmt"

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

	go func() {
		ch <- 100
	}()

	v, ok := <-ch
	fmt.Println(v, ok) // 100 true
}

channelのクローズ

チャネルを明示的にクローズしたいん場合はclose()を使います。

close(チャネル名)

先ほどの例をcloseを挿入して書き直すと以下のようになります。

package main

import "fmt"

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

	go func() {
		ch <- 100
	}()

	v := <-ch

	fmt.Println(v) // 100

    close(ch)
}

goroutineでのchannelの利用

channelのバッファについて

チャネルでは、バッファのサイズを指定しないと、受信側がデータを受け取るまで送信側はブロックされてしまいます。

連続で送信したい場合には、バッファサイズを指定する必要があります。例えばバッファサイズを1に指定すると、1つだけバッファされ、2つ目の送信時にブロックされるようになります。

これを試してみます。以下、実験コードです。

バッファを指定しない場合

以下のコードを実行すると、”send:“は1度も表示されません。これは、その上のch <- iで送信したデータが受信されるまでブロックしているからです。

package main

import (
	"fmt"
	"time"
)

func send(ch chan<- int) {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("send:", i)
	}
}

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

	go send(ch)

	time.Sleep(time.Second * 10)
}

バッファを1にした場合

バッファサイズを1にした場合は、send: 0と表示されます。バッファが1つあるので2つ目の送信でブロックされました。

package main

import (
	"fmt"
	"time"
)

func send(ch chan<- int) {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("send:", i)
	}
}

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

	go send(ch)

	time.Sleep(time.Second * 10)
}

// send: 0

バッファを5にした場合

バッファを5にすると、以下のようになります。このように、バッファサイズを指定することで、すぐにはブロックされないようにすることが可能です。

package main

import (
	"fmt"
	"time"
)

func send(ch chan<- int) {
	for i := 0; i < 10; i++ {
		ch <- i
		fmt.Println("send:", i)
	}
}

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

	go send(ch)

	time.Sleep(time.Second * 10)
}
// send: 0
// send: 1
// send: 2
// send: 3
// send: 4

送信側と受信側の処理速度に違いがある、処理の速度にばらつきがある場合などにバッファを用意することで、並列度を向上させることが可能です。

channelとselect文

select文を使うことで、複数のチャネルからのデータを待ち受けることが可能です。

以下は、send1とはch1で送受信、send2とはch2で送受信する例です。

受信側では、selectを使ってデータを受信しています。

このようにすることで、複数のチャネルのうち、データを準備できたチャネルからデータを受信して処理することが可能になります。

package main

import (
	"fmt"
	"sync"
)

func send1(ch chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		ch <- i

	}
}

func send2(ch chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		ch <- 100 + i
	}
}

func main() {
	var wg sync.WaitGroup
	ch1 := make(chan int)
	ch2 := make(chan int)

	wg.Add(2)
	go send1(ch1, &wg)
	go send2(ch2, &wg)

	for i := 0; i < 20; i++ {
		select {
		case v1 := <-ch1:
			fmt.Println("Received from ch1:", v1)
		case v2 := <-ch2:
			fmt.Println("Received from ch2:", v2)
		}
	}
	wg.Wait()
}

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

※なお、ch1とch2の受信順は実行のたびに変化します。

Received from ch2: 100
Received from ch2: 101
Received from ch1: 0
Received from ch1: 1
Received from ch1: 2
Received from ch1: 3
Received from ch1: 4
Received from ch1: 5
Received from ch1: 6
Received from ch1: 7
Received from ch1: 8
Received from ch1: 9
Received from ch2: 102
Received from ch2: 103
Received from ch2: 104
Received from ch2: 105
Received from ch2: 106
Received from ch2: 107
Received from ch2: 108
Received from ch2: 109

プログラム中のsync.WaitGroupは、同期のための機構です。例ではAdd(2)とし、send0, send1でそれぞれ、wg.Done()を実行して完了を通知しています。wg.Wait()は、Addで指定された数のDoneが実行されるまで待ちます。

これにより、snnd0/1が実行完了するまで待つ形になります

上のプログラムでは、手前でチャネルの出力を全て受け取ってから終了しているので必要ないですが、とりあえず。

その他

for~rangeによる繰り返しにもチャネルを利用することが可能です。こちらについては以下を参考にしてください。

あわせて読みたい
Go言語の ゴルーチンとチャネルでfor~rangeによる繰り返しを実現する方法
Go言語の ゴルーチンとチャネルでfor~rangeによる繰り返しを実現する方法

まとめ

以上、チャネルについて説明してきました。ゴルーチンとチャネルのおかげでGo言語では比較的手軽に並列実行を記述することが可能です。このあたりはよくできている言語だなと感じています。

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

記事URLをコピーしました