Go言語の大きな特徴である並行処理。前回はGoroutineについて学びました。今回は、Goroutine間で安全にデータをやり取りするための仕組みであるChannel(チャネル)と、複数のチャネルを扱うためのselect文について学びましょう!
Channel(チャネル)とは?
チャネルは、Goroutine間でデータを送受信するためのパイプのようなものです。 Go言語では「通信によってメモリを共有する」という考え方が推奨されており、チャネルはその中心的な役割を担います。
チャネルを使うことで、複数のGoroutineが同時に同じデータにアクセスしようとして起こる問題(競合状態)を防ぎ、安全なデータのやり取りが可能になります。
チャネルは以下の特徴を持っています:
- 型付けされている: チャネルは特定の型のデータしか送受信できません(例:
chan int
はint型専用)。 - 送受信操作:
<-
演算子を使ってデータの送信と受信を行います。 - 同期機構: デフォルト(バッファなしチャネル)では、送信側と受信側が揃うまで処理がブロックされます。これによりGoroutine間の同期を取ることができます。
チャネルの作成
チャネルは組み込みのmake
関数を使って作成します。
package main
import "fmt"
func main() {
// int型の値を送受信するチャネルを作成
ch := make(chan int)
fmt.Printf("チャネルの型: %T, 値: %v\n", ch, ch) // 出力例: チャネルの型: chan int, 値: 0xc00001a0e0
// string型の値を送受信するチャネルを作成
strCh := make(chan string)
fmt.Printf("チャネルの型: %T, 値: %v\n", strCh, strCh)
// bool型の値を送受信するチャネルを作成
boolCh := make(chan bool)
fmt.Printf("チャネルの型: %T, 値: %v\n", boolCh, boolCh)
// makeで作成しない場合、チャネルのゼロ値はnil
var nilCh chan int
fmt.Printf("nilチャネルの型: %T, 値: %v\n", nilCh, nilCh) // 出力例: nilチャネルの型: chan int, 値:
}
make
で作成しない場合、チャネル変数の値はnil
となり、そのまま送受信しようとするとデッドロック(プログラムが停止してしまう状態)になります。
チャネルへの送信と受信
<-
演算子を使ってチャネルにデータを送信したり、チャネルからデータを受信したりします。
- 送信:
チャネル変数 <- 値
- 受信:
変数 := <-チャネル変数
package main
import (
"fmt"
"time"
)
func sendData(ch chan string) {
fmt.Println("Goroutine: 送信するよ! ")
ch <- "こんにちは、チャネル!" // チャネルに文字列を送信
fmt.Println("Goroutine: 送信完了!")
}
func main() {
ch := make(chan string) // string型のチャネルを作成
go sendData(ch) // Goroutineを起動してsendData関数を実行
fmt.Println("Main: データ受信待ち... ")
// チャネルからデータを受信 (データが送信されるまでここでブロックされる)
message := <-ch
fmt.Printf("Main: 受信したデータ: %s\n", message)
fmt.Println("Main: 処理完了!")
time.Sleep(1 * time.Second) // Goroutineの終了を待つ(デモ用)
}
上記の例では、sendData
Goroutineがチャネルにデータを送信するまで、main
Goroutineのmessage := <-ch
の部分で処理が待機(ブロック)されます。データが送信されると、main
Goroutineは受信を完了し、処理を続行します。
バッファなしチャネル vs バッファ付きチャネル
make
で作成する際、第二引数でバッファサイズを指定できます。
- バッファなしチャネル (Unbuffered Channel):
make(chan T)
- サイズが0のチャネル。
- 送信操作は、対応する受信操作が準備できるまでブロックされる。
- 受信操作は、対応する送信操作が準備できるまでブロックされる。
- Goroutine間の確実な同期が必要な場合に適しています。
- バッファ付きチャネル (Buffered Channel):
make(chan T, capacity)
- サイズが
capacity
のバッファを持つチャネル。 - 送信操作は、バッファがいっぱいになるまでブロックされない。
- 受信操作は、バッファが空になるまでブロックされない。
- 一時的にデータを溜めておくキューのような使い方ができます。 FIFO(先入れ先出し)の順序が保証されます。
- サイズが
package main
import "fmt"
func main() {
// バッファなしチャネル (同期)
// unbufCh := make(chan int)
// unbufCh <- 1 // ここで受信側がいないとデッドロック!
// go func() { <-unbufCh }() // 別Goroutineで受信すればOK
// バッファ付きチャネル (非同期)
bufCh := make(chan int, 2) // サイズ2のバッファ
bufCh <- 1 // ブロックされない
bufCh <- 2 // ブロックされない
// bufCh <- 3 // バッファが一杯なので、ここで受信されるまでブロックされる
fmt.Println("バッファ付きチャネルから受信:", <-bufCh)
fmt.Println("バッファ付きチャネルから受信:", <-bufCh)
}
バッファ付きチャネルは、送信側と受信側の処理速度に差がある場合などに便利ですが、バッファがいっぱいになると送信側が、バッファが空になると受信側がブロックされる点はバッファなしチャネルと同じです。
チャネルのクローズ
close
関数を使ってチャネルを閉じることができます。これは、もうこれ以上データを送信しないことを受信側に伝えるための合図です。
package main
import "fmt"
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
go func() {
for {
// 受信操作は2つの値を受け取れる
// j: 受信した値
// ok: チャネルが開いているか (true: 開いている, false: 閉じている)
j, ok := <-jobs
if ok {
fmt.Println("受信したジョブ:", j)
} else {
fmt.Println("全ジョブ受信完了!チャネルは閉じられました。")
done <- true // 完了を通知
return
}
}
}()
// 3つのジョブを送信
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("送信したジョブ:", j)
}
// これ以上ジョブは送信しないのでチャネルを閉じる
close(jobs)
fmt.Println("全ジョブ送信完了。チャネルを閉じました。")
// 受信側Goroutineの完了を待つ
<-done
}
注意点:
- 閉じたチャネルに送信しようとするとパニックが発生します 。
- 閉じたチャネルから受信すると、バッファに残っている値を受信し続けます。バッファが空になると、そのチャネルの型のゼロ値(intなら0, stringなら””など)が即座に返され、2つ目の戻り値
ok
がfalse
になります。 - チャネルを閉じるのは、通常は送信側の役割です。
- すでに閉じているチャネルを再度
close
しようとするとパニックが発生します。
for range
を使ってチャネルから受信することもできます。この場合、チャネルが閉じられると自動的にループが終了します。
package main
import "fmt"
func main() {
queue := make(chan string, 2)
queue <- "one"
queue <- "two"
close(queue) // closeしないとfor rangeがデッドロックになる!
// チャネルが閉じられるまで値を受信し続ける
for elem := range queue {
fmt.Println(elem)
}
// 出力:
// one
// two
}
select文
select
文は、複数のチャネル操作(送受信)を同時に待機し、いずれか一つが実行可能になったときにその操作を実行するための仕組みです。switch
文に似ていますが、チャネル操作専用です。
基本的な使い方
select
文はcase
節を持ち、各case
はチャネルの送受信操作に対応します。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string)
c2 := make(chan string)
// 1秒後にc1へ送信
go func() {
time.Sleep(1 * time.Second)
c1 <- "one"
}()
// 2秒後にc2へ送信
go func() {
time.Sleep(2 * time.Second)
c2 <- "two"
}()
// 2つのチャネルの受信を待つ
// selectはどちらかのcaseが実行可能になるまでブロックする
for i := 0; i < 2; i++ {
select {
case msg1 := <-c1:
fmt.Println("received", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
}
// 出力例 (実行順は不定):
// received one
// received two
}
複数のcase
が同時に実行可能になった場合、select
はそのうちの一つをランダムに選択して実行します。
defaultケース
select
文にdefault
ケースを追加すると、どのcase
もすぐに実行できない場合にdefault
ケースが実行されます。これにより、select
文がブロックしなくなります(ノンブロッキング操作)。
package main
import (
"fmt"
"time"
)
func main() {
messages := make(chan string)
signals := make(chan bool)
// 何も送信しないGoroutine (デモ用)
// go func() {
// time.Sleep(1 * time.Second)
// messages <- "hi"
// }()
// すぐに実行できるcaseがないかチェック
select {
case msg := <-messages:
fmt.Println("received message", msg)
default: // どのcaseもすぐに実行できない場合
fmt.Println("no message received")
}
msg := "hi"
// 送信を試みる (messagesチャネルはバッファなしなので受信側がいないとブロックする)
select {
case messages <- msg: // 受信側がいないため、すぐには実行できない
fmt.Println("sent message", msg)
default: // すぐに送信できない場合
fmt.Println("no message sent")
}
// 複数のcaseを持つノンブロッキングselect
select {
case msg := <-messages:
fmt.Println("received message", msg)
case sig := <-signals:
fmt.Println("received signal", sig)
default:
fmt.Println("no activity")
}
// 出力:
// no message received
// no message sent
// no activity
}
タイムアウト処理
select
とtime.After
チャネルを組み合わせることで、タイムアウト処理を簡単に実装できます。time.After
は指定した時間が経過した後に現在時刻を送信するチャネルを返します。
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
// 2秒かかる処理
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
// c1からの受信を待つが、1秒でタイムアウトする
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second): // 1秒後に値を受信するチャネル
fmt.Println("timeout 1")
}
c2 := make(chan string, 1)
go func() {
// 0.5秒かかる処理
time.Sleep(500 * time.Millisecond)
c2 <- "result 2"
}()
// c2からの受信を待つが、1秒でタイムアウトする
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 2")
}
// 出力:
// timeout 1
// result 2
}
最初のselect
では、c1
からの受信(2秒後)よりもtime.After
(1秒後)が先に実行可能になるため、タイムアウトします。二番目のselect
では、c2
からの受信(0.5秒後)がtime.After
(1秒後)よりも先に実行可能になるため、結果を受信できます。
まとめ
今回は、Goの並行処理において重要な役割を果たすチャネルとselect文について学びました。
- チャネル: Goroutine間で安全にデータを送受信するための型付きパイプ。
make
で作成し、<-
で送受信する。バッファなし(同期的)とバッファ付き(非同期的)がある。close
で閉じることができる。 - select文: 複数のチャネル操作を待ち受け、最初に実行可能になったものを(ランダムに)実行する。
default
でノンブロッキングに、time.After
と組み合わせてタイムアウト処理を実装できる。
これらの機能を使いこなすことで、Goの強力な並行処理をより安全かつ効率的に実装できるようになります。最初は少し難しく感じるかもしれませんが、実際にコードを書いて動かしてみるのが一番の近道です!どんどん試してみてくださいね。
次回は、並行処理をより安全に行うためのsync
パッケージ(MutexやWaitGroupなど)について見ていきましょう!