Go言語のチャネルの基本と注意点|ゴルーチンでデータを送受信する方法
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を用いたデータ送受信方法
チャネルにデータを送る
チャネルにデータを送る場合は、チャネル名<-データ
という構文を使います。
チャネル名 <- データ
チャネルからデータを受け取る
チャネルからデータを受け取る場合は、変数<-チャネル
になります。
送信時はチャネルが左辺、受信時はチャネルが右辺になる点に注意してください
変数 <- チャネル名
以下、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)
}
このように複数のスレッド間でデータを受け渡すのにチャネルを使うことができます。
このプログラムでは適当な時間main
をSleep
させていますが、終了まで待つことも可能です。後ルーチンの終了を待つ方法については以下の記事を参考にしてください。
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言語では比較的手軽に並列実行を記述することが可能です。このあたりはよくできている言語だなと感じています。