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

Go言語のチャネルの基本と注意点|ゴルーチンでデータを送受信する方法

Aru

Go言語のチャネルは、ゴルーチン間でデータをやり取りするための仕組みです。本記事では、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でゴルーチンとして呼び出しています。この関数ではチャネルchに100を書き込んでいます。

メインプログラムではchから値を受信しています。

なお、チャネルからの受信では、以下のプログラムのように戻り値を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
}

send/recvを行うプログラムサンプル

チャネルを使って2つのゴルーチン間で値を受け渡す例です。send関数が送信側、recv関数が受信側になります。それぞれ、チャネルが送信・受信であることを明示するためにchan<-<-chanと引数に宣言している部分にも注意してください。

sendでは20msに一回値を送信し、recvでは100を受け取るまで繰り返し受信しています。

なお、mainプログラムは一定時間(3秒)待ってから終了しています(20msに1回なので100個送るのに2000ms=2secとなります。余裕をみて3秒に設定しています)。

package main

import (
	"fmt"
	"time"
)

func send(ch chan<- int) {
	for i := 1; i <= 100; i++ {
		ch <- i
		time.Sleep(time.Millisecond * 20)
	}
	fmt.Println("send end")
}

func recv(ch <-chan int) {
	for true {
		x := <-ch
		fmt.Println("recv = ", x)
		if x == 100 {
			fmt.Println("recv end")
		}
	}
}
func main() {
	ch := make(chan int)

	go send(ch)
	go recv(ch)

	time.Sleep(3 * time.Second)
}

このように複数のスレッド間でデータを受け渡すのにチャネルを使うことができます。

このプログラムでは適当な時間mainSleepさせていますが、終了まで待つことも可能です。後ルーチンの終了を待つ方法については以下の記事を参考にしてください。

終了同期の方法についてはこちら
ゴルーチン(goroutine)とチャネルを使った並行処理の実践例|Go言語
ゴルーチン(goroutine)とチャネルを使った並行処理の実践例|Go言語

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)
}

バッファサイズの意味と活用法

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

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

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

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

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

main関数では、chからデータを受信していないので、送信側はいつまでも受信を待ち続けることになります。

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と表示されます。

バッファがあるので、次のsendを行うことができます。ただし、次の送信はバッファがいっぱいなので受信されるまでブロックされます。

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

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

select文による多重待ち合わせ

プログラムによっては、複数のチャネルからの受信を待って、データが送られてきたチャネルに対して都度処理したい場合があります。

これを実現するのがselect文です。

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

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

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()
}

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

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

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

※なお、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言語では比較的手軽に並列実行を記述することが可能です。このあたりはよくできている言語だなと感じています。

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

ABOUT ME
ある/Aru
ある/Aru
IT&機械学習エンジニア/ファイナンシャルプランナー(CFP®)
専門分野は並列処理・画像処理・機械学習・ディープラーニング。プログラミング言語はC, C++, Go, Pythonを中心として色々利用。現在は、Kaggle, 競プロなどをしながら悠々自適に活動中
記事URLをコピーしました