Go 言語のチャネル

Go のチャネルとは?

チャネル (channel) は Go 言語に組み込まれた変数の種類のひとつです。

チャネルは goroutine 間でデータの送受信をするために使います

goroutine は Go 言語のもつ並行処理の仕組みです。チャネルを利用する例には goroutine を使いますので、まだ学習していない人は先に 「Go 言語の goroutine」をご覧ください。

Go のチャネルの作成

チャネルはビルトイン関数の make() を用いて、次の形式で作成します。

チャネル変数 := make(chan 送受信するデータ型)

例えば、文字列 (string) を送受信するチャネル ch を作成するには次のようにします。

ch := make(chan string)

ここで作成したチャネルは「バッファー無し」 (unbuffered) のチャネルになります。

チャネルは参照型の変数になります。このため関数やメソッドに渡す場合、ポインターとして明示的に書かなくとも自動的に参照が渡されることになります。

Go のチャネルの方向

chan<- と書くことで 「チャネルにデータを送る」という方向を明示できます。例えば、「文字列を受け取るチャネルの型」として chan<- string と書けます。

<-chan と書くことで 「チャネルからデータを受け取る」方向を明示できます。例えば、「文字列を送るチャネルの型」として <-chan string と書けます。

こうした書き方は、関数やメソッドにチャネルを渡す時に、その関数でのチャネルの役割を記述するのによく使われます。

Go のチャネルを用いたデータの送受信

チャネルにデータを送るには、次の形式で行います。

チャネル <- データ

例えば、「文字列型を送受信するチャネル」の ch に対して、文字列 "Hello" を送るには、次のようにします。

ch <- "Hello"

チャネルからデータを受取るには、次の形式で行います。

データを受け取る変数 <- チャネル

例えば、「文字列型を送受信するチャネル」の ch から、文字列を受け取るには、次のようにします。

s := <-ch

goroutine で実際に使用する例をみてみましょう。

次の例では goroutine を二つ作り、ひとつは recv() 関数を実行してチャネルから文字列を受け取ります。 もう一方では send() 関数を実行して、チャネルに文字列を送ります。

package main

import (
	"fmt"
	"sync"
)

func recv(ch <-chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	s := <-ch // チャネルから文字列を受け取る
	fmt.Println("[R]", s)
}

func send(ch chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("[S] Sending...")
	ch <- "Hello" // チャネルに文字列を送る
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	// チャネルの作成
	ch := make(chan string)
	// チャネルから文字列を受取る側
	go recv(ch, &wg)
	// チャネルに文字列を送る側
	go send(ch, &wg)
	// 両方の goroutine が終わるのを待つ
	wg.Wait()
}
main.go

この実行結果は次のようになります。

go run main.go
[S] Sending...
[R] Hello

[R] という文字は recv() 関数側で表示しています。確かに Hello という文字が受け取れたことがわかりますね。

Go のチャネルのクローズ

チャネルに送るデータがこれ以上ない状態になったら、明示的に close() 関数を呼ぶことで、 チャネルをクローズすることができます。

close(ch)

クローズしたチャネルにデータを送ると、パニック状態 (プログラムの強制終了) となります。

クローズしたチャネルから読み取りを行うと、ブロックせずに直ちにゼロ値が返ります。

「ゼロ値」というのは int 型なら 0、bool 型なら false、string なら "" (空文字列) です。

バッファのあるチャネルから読み取る場合は、チャネルをクローズした後でもバッファに読み取られていないデータが残っていれば読みとることができます。チャネルのバッファについては次の節を見てください。

package main

import "fmt"

func main() {
	ch := make(chan int)
	close(ch)
	m := <-ch
	fmt.Println(m) // 0 
}

「正しい読み取りデータとしての 0」と「クローズしたチャネルから読み取りを試みたことによる無意味なゼロ値としての 0」 を区別するには良い方法があります。

実はチャネルからの受け取り式 (receive expression) では、データをもう一つ受け取ることができます。 チャネルの状態が閉じているまたは空の場合に、二つ目の値が false となります。

package main

import "fmt"

func main() {
	ch := make(chan int)
	close(ch)
	m, ok := <-ch // チャネルが閉じている時は ok は false となる
	if ok {
		fmt.Println(m)
	} else {
		fmt.Println("channel closed")
	}
}

上の ok が false のときは、m には意味のないゼロ値が代入されるので無視できます。

Go のチャネルのバッファ

Go のバッファ無しのチャネルのブロック

ここまでみてきたバッファは、「バッファのない」(unbuffered) チャネルです。

バッファ無しのチャネルにデータを送ると、データが受け取られるまでブロックします。

もし、受け取り側が1回しかチャネルからデータを受け取らずに、 送信側が2回データを送信したらどうなるでしょうか。

package main

import (
	"fmt"
	"sync"
)

func recv(ch <-chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	s := <-ch // 1回だけ受け取り
	fmt.Println("[R]", s)
}

func send(ch chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("[S] Sending...")
	ch <- "Hello"
	ch <- "World" // ここは受け取られないのでブロック
	close(ch)
	fmt.Println("[S] Done")
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	ch := make(chan string)
	go recv(ch, &wg)
	go send(ch, &wg)
	wg.Wait()
}

読み取り側が一度の読み取り操作で goroutine を終了してしまっているため、 "World" という文字列は読み取られる見込みがなくなってしまうので、永久に返らない状況 (デッドロック) となります。

Go のバッファ付きチャネル

チャネルを作成する時に、 make() 関数の第二引数でチャネルのバッファサイズを指定できます。

チャネル変数 := make(chan 送受信するデータ型, バッファサイズ)

こうして作成されたチャネルはバッファ付き (buffered) のチャネルになります。

バッファがある場合、バッファサイズ内であればデータを繰り返し送信しても、送信操作はブロックしません。

次の例では send() 関数内では3回データを送っていますが、 recv() 関数では1回しか受け取っていません。 しかし、チャネルのバッファサイズが3であるため、データの送り側がブロックしていません。

package main

import (
	"fmt"
	"sync"
)

func recv(ch <-chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	s := <-ch
	fmt.Println("[R]", s)
}

func send(ch chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("[S] Sending...")
	ch <- "Hello"
	ch <- "World" // バッファがあるので読み取られなくてもブロックしない
	ch <- "Bye!"
	close(ch)
	fmt.Println("[S] Done")
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	// チャネルの作成
	ch := make(chan string, 3) // バッファサイズを指定
	go recv(ch, &wg)
	go send(ch, &wg)
	wg.Wait()
	// もしここまでで読み取られていないデータがあったら読み取る
	for s := range ch {
		fmt.Println("[Z]", s)
	}
}

この実行結果は次のようになります。

go run main.go
[S] Sending...
[S] Done
[R] Hello
[Z] World
[Z] Bye!

まだ受け取られていないデータは、 recv() 関数、 send() 関数の両方が終了した後に読み込まれています。

for-range については次の節で説明しています。

Go のチャネルからの繰り返しのデータ受け取り

for ループによるチャネルからの繰り返しデータ受け取り

チャネルから繰り返しデータを受け取るには、次のように for ループを使う方法が考えられます。

for {
	s, ok := <-ch
	if !ok {
		fmt.Println("* channel closed")
		break
	}
}

for ループの終了条件に注意しましょう。チャネルがクローズされていないかチェックし、クローズしていたらループを終了するなどの処理は必須です。

実行例をみてみましょう。

package main

import (
	"fmt"
	"sync"
)

func recv(ch <-chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		s, ok := <-ch
		if !ok {
			fmt.Println("* channel closed")
			break
		}
		fmt.Println("[R]", s)
	}
}

func send(ch chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("[S] Sending...")
	for i := 0; i < 5; i++ {
		ch <- fmt.Sprintf("Hello %d", i)
	}
	fmt.Println("[S] Done")
	close(ch)
}

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	ch := make(chan string)
	go recv(ch, &wg)
	go send(ch, &wg)
	wg.Wait()
}
main.go
go run main.go
[S] Sending...
[R] Hello 0
[R] Hello 1
[R] Hello 2
[R] Hello 3
[S] Done
[R] Hello 4
* channel closed

for-range によるチャネルからの繰り返しデータ受取り

チャネルからデータを受け取るには、 for-range ループを使うこともできます。

for 変数 := range チャネル {
}

チャネルの for-range ループでは、バッファ無しのチャネルではチャネルがクローズされたところでループが終了します。バッファありのチャネルの場合は、チャネルがクローズされて受け取りデータがバッファに残らない状態になったところでループが終了します。

上記の recv() 関数を for-range を使って書き換えると次のようになります。

func recv(ch <-chan string, wg *sync.WaitGroup) {
	defer wg.Done()
	for s := range ch {
		fmt.Println("[R]", s)
	}
}

Go のチャネルの select

select 文を使うと、複数のチャネルからのデータを待つことができます。

select 文は次の形で使います。case 文と似た形です。

select {
case チャネル1からのデータ受け取り待ち:
	// ステートメント (任意)
case チャネル2からのデータ受け取り待ち:
  // ステートメント (任意)
...
}

複数のチャネルからの受け取り待ちを行い、待ちが返った箇所の case のステートメントがもしあれば、それらが実行されます。

例えばチャネル ch1ch2 からデータの受け取り待ちを行い、受け取ったデータを出力するなら、次のようにします。

select {
case s := <-ch1:
	fmt.Println(s)
case i := <-ch2:
	fmt.Println(i)
}

データ受け取りを繰り返し行う場合は、例えば for 文と組み合わせて次のようにします。

for {
	select {
	case s := <-ch1:
		fmt.Println(s)
	case i := <-ch2:
		fmt.Println(i)
	case <-done:
		return
	}
}

<-done は、なにをしているのでしょうか?

この done はループの終了を明示的に行うために用意したチャネルです。 この箇所では、なんのデータも受け取りませんが、チャネルがクローズされるなどして受け取り式が返った時に return 文が実行されることになります。

for-select を使った実行例は次のようになります。

package main

import (
	"fmt"
	"sync"
	"time"
)

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

	go func() {
		for {
			select {
			case c := <-ch1:
				fmt.Printf("[R1] %c\n", c)
			case i := <-ch2:
				fmt.Println("[R2]", i)
			case <-done:
				fmt.Println("Done!")
				return
			}
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		for c := 'A'; c <= 'C'; c++ {
			ch1 <- c
			time.Sleep(time.Second)
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 1; i < 5; i++ {
			ch2 <- i
			time.Sleep(500 * time.Millisecond)
		}
	}()

	wg.Wait()

	close(done)

	time.Sleep(time.Second)
}
main.go

この実行結果は次のようになります。

go run main.go
[R1] A
[R2] 1
[R2] 2
[R1] B
[R2] 3
[R2] 4
[R1] C
Done!

繰り返しになりますが、for-select ループでは終了条件に注意する必要があります。

case でデータの式が記載されているチャネルを、 データの送信側からクローズしてしまうと、case が常に返る状況になります。 なぜなら、上で説明したように、クローズされたチャネルからのデータの受け取り式は直ちに返り、バッファ内のデータかゼロ値を受け取るからです。

このため for-select ではデータを受け取っているチャネルを直接閉じて終了とするのではなく、ループ終了のためのチャネルを別途用意して、それを使ってループを終了条件に使うことがよく行われます。

以上、チャネルについて説明しました。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Go 言語入門