チャネル(Channels) - Golang learning step 2-5
- 公開日
- カテゴリ:GoingDeeper
- タグ:Golang,roadmap.sh,学習メモ
roadmap.sh > Go > Going Deeper > Channels の学習を進めていきます。
※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。
contents
- 開発環境
- 参考 URL
- チャネル(Channels)
- チャネルの基本構文
- チャネルへの送信と受信
- チャネルの特徴
- チャネルのブロッキング
- バッファなしチャネル
- バッファありチャネル
- チャネルのクローズ
- Select 文
- チャネルの注意点
- ベストプラクティス
開発環境
- チップ: Apple M2 Pro
- OS: macOS Sonoma
- go version: go1.23.2 darwin/arm64
参考 URL
- [official] Channels
- [official] Effective Go: Channels
- [article] Go by Example: Channels
- [article] Channels in Golang
- [video] Channels
- [video] Golang Channel Basics You must Know!
チャネル(Channels)
Go のチャネル(channels)は、ゴルーチン間でデータを安全かつ効率的に送受信するための機構です。Go の並行処理の基本的なコンセプトである CSP(Communicating Sequential Processes)に基づいており、チャネルを使ってゴルーチン間の通信と同期が可能になります。チャネルはゴルーチン間でデータを安全に受け渡すためのパイプラインとして機能します。
チャネルの基本構文
チャネルは make
関数を使って作成します。たとえば、整数型のチャネルを作成する場合は以下のようにします。
ch := make(chan int)
このコードで作成した ch
というチャネルは、int 型のデータを送受信できます。
チャネルへの送信と受信
チャネルにデータを送信するには、以下のように使います。
ch <- 10 // 10をチャネルに送信
逆に、チャネルからデータを受信するには以下のようにします。
value := <-ch // チャネルからデータを受信
一連の流れを記すと以下になります。
package main
import "fmt"
func main() {
ch := make(chan int) // バッファなしチャネル
go func() {
ch <- 10
}()
∂
value := <-ch
fmt.Printf("受信した値は %d です\n", value)
}
チャネルの特徴
- 型安全性
- チャネルは特定の型のデータのみを扱うことができる
- コンパイル時に型チェックが行われる
- 同期機能
- デフォルト(バッファサイズを指定しない場合)では送受信操作がブロッキングされる
- 送信側と受信側の同期を自動的に行う
チャネルのブロッキング
- 送信のブロック
- あるゴルーチンがチャネルにデータを送信しようとする際、受信する側の準備ができていない場合、送信を行っているゴルーチンは一時停止します。具体的には、受信側がチャネルからデータを受け取るまで、そのゴルーチンは次の処理に進めません。
- 受信のブロック
- 同様に、あるゴルーチンがチャネルからデータを受信しようとする際、送信する側の準備ができていなければ、そのゴルーチンは停止します。送信側がデータを送信するまで、受信ゴルーチンは待機して次の処理に進むことができません。
この「ブロッキング」動作によって、チャネルはゴルーチン間の同期を自然に実現します。たとえば、あるゴルーチンがデータを生成し、別のゴルーチンがそのデータを処理する際、受信側はデータが用意されるまで待機し、逆にデータを送信する側は受信の準備ができるまで待機します。これにより、ゴルーチン間でデータのタイミングが自動的に調整されるため、安全で効率的な並行処理が可能になります。
このように、チャネルを介してゴルーチン間でデータの受け渡しが行われる際、送信側や受信側が互いの準備が整うまで処理を一時停止するため、安全なデータ同期が可能になります。
バッファなしチャネル
バッファなしチャネルは、バッファ(容量)を指定しないチャネルです。バッファを指定しない場合、チャネルの送信と受信は必ず 1 対 1 で行われる制約の元に動作します。例えば、受信しない限り送信側で前述したブロッキング(待機)が発生します。
バッファなしチャネルの場合、送信側がデータを送信するためには受信側が必ず待機している必要があります。つまり、受信側の準備が整うまでは、送信側のゴルーチンは次の処理に進めず、一時停止(ブロック)されます。これは、チャネルにデータを送信するためには、受信側がデータを受け取る「準備」ができていることが前提条件であるためです。この性質により、チャネルを介したゴルーチン間の同期が保証され、安全なデータ受け渡しが実現します。
// バッファなしチャネル
ch := make(chan int)
【送信側】 【チャネル】 【受信側】
ch <- 42 --×->| |
(ブロック中) | |
| | <-ch で受信待ち(value := <-ch)
(ブロック解除) | |
送信完了 ---->| |
| | 42 を受信
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) // バッファなしチャネル
// 送信側の処理
go func() {
fmt.Println("送信準備完了")
ch <- 42 // ここでブロックされる
fmt.Println("送信完了") // 受信されるまでここには到達しない
}()
// メインルーチンで少し待機
time.Sleep(2 * time.Second)
// 受信側の処理
fmt.Println("受信準備完了")
value := <-ch // 受信処理
fmt.Println("受信した値:", value)
time.Sleep(1 * time.Second) // 送信完了のメッセージを確認するため待機
}
// 出力:
// 送信準備完了
// 受信準備完了
// 受信した値: 42
// 送信完了
バッファありチャネル
通常、チャネルはバッファなし(容量ゼロ)ですが、バッファありチャネルも作成可能です。バッファありチャネルの場合、受信せずとも指定したバッファの数までは送信が可能になります。以下は容量を 2 にしたバッファ付きチャネルの例です。
ch := make(chan int, 2) // バッファサイズ 2 のチャネル
// バッファに空きがある限りブロックされない
ch <- 1 // ブロックされない
ch <- 2 // ブロックされない
ch <- 3 // ここでブロックされる(バッファが満杯のため)
バッファ付きチャネルの場合、バッファが満たされるまでは送信はブロックされません。バッファがいっぱいになると、送信側はブロックされ、バッファに空きができるまで待機します。
// バッファサイズ2のチャネル
ch := make(chan int, 2)
【送信側】 【チャネル(バッファ)】 【受信側】
ch <- 1 ----> |[1][ ]| まだ受信していない
|[1][ ]|
ch <- 2 ----> |[1][2]| まだ受信していない
|[1][2]|
ch <- 3 --×-> |[1][2]| (バッファ満杯なのでブロック)
|[2][ ]| v := <- ch
ch <- 3 ----> |[2][3]|
package main
import (
"fmt"
"time"
)
func main() {
bufCh := make(chan int, 2)
go func() {
bufCh <- 1 // ブロックされない
fmt.Println("1つ目の送信完了")
bufCh <- 2 // ブロックされない
fmt.Println("2つ目の送信完了")
bufCh <- 3 // ブロックされる
fmt.Println("3つ目の送信完了")
}()
time.Sleep(2 * time.Second)
v := <-bufCh
fmt.Println(v)
time.Sleep(1 * time.Second)
v = <-bufCh
fmt.Println(v)
time.Sleep(1 * time.Second)
v = <-bufCh
fmt.Println(v)
time.Sleep(1 * time.Second)
}
// 出力:
// 1つ目の送信完了
// 2つ目の送信完了
// 1
// 3つ目の送信完了
// 2
// 3
チャネルのクローズ
close
関数を使ってチャネルをクローズすることができます。クローズされたチャネルには送信できませんが、受信は可能です。クローズされたことを確認するために、for
ループと range
を使って受信し続けることができます。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5)
// データを送信するゴルーチン
go func() {
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("%d を送信しました\n", i)
}
close(ch) // チャネルをクローズ
fmt.Println("チャネルをクローズしました")
}()
time.Sleep(2 * time.Second)
fmt.Println("今から受信していきます")
// チャネルからデータを受信し続ける
for value := range ch {
fmt.Println("受信:", value)
}
fmt.Println("受信が終了しました")
}
// 出力:
// 0 を送信しました
// 1 を送信しました
// 2 を送信しました
// 3 を送信しました
// 4 を送信しました
// チャネルをクローズしました
// 今から受信していきます
// 受信: 0
// 受信: 1
// 受信: 2
// 受信: 3
// 受信: 4
// 受信が終了しました
Select 文
複数のチャネルを同時に扱う場合、select
文を使用すると便利です。select
を使うと、複数のチャネルからの送受信を待機し、どれか一つのケースが準備できたらそれを実行します。
Select 文の基本動作
- 複数のチャネル操作を待ち受ける
- 最初に準備できた操作を実行する
- どの操作も準備できていない場合はブロック
- 複数の操作が同時に準備できた場合はランダムに1つ選択
基本的な使用例
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
// 送信側ゴルーチン1
go func() {
time.Sleep(2 * time.Second) // 2 秒待機
ch1 <- "one"
}()
// 送信側ゴルーチン2
go func() {
time.Sleep(1 * time.Second) // 1 秒待機(こちらが先に送信される)
ch2 <- "two"
}()
// 受信側(select)
select {
case msg1 := <-ch1:
fmt.Println("受信ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("受信ch2:", msg2)
}
}
// 出力:
// 受信ch2: two
よくある使用パターン
1. タイムアウト処理
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second) // 2 秒待機
ch <- "処理完了"
}()
select {
case msg := <-ch:
fmt.Println("受信:", msg)
case <-time.After(1 * time.Second): // 1 秒経過でタイムアウトになる
fmt.Println("タイムアウト")
}
}
// 出力:
// タイムアウト
2. デフォルトケース(ノンブロッキング操作)
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
select {
case msg := <-ch:
fmt.Println("受信:", msg)
default:
fmt.Println("データなし") // チャネルが空の場合すぐに実行
}
}
// 出力:
// データなし
3. 送受信の両方を待ち受ける
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string) // メッセージ送信用チャネル
done := make(chan bool) // 処理中断用チャネル
// 受信側ゴルーチン(別のゴルーチンで待ち受ける人)
go func() {
time.Sleep(2 * time.Second) // 2秒後に受信する想定
msg := <-ch
fmt.Println("受信完了:", msg)
}()
// 送信用ゴルーチン
go func() {
time.Sleep(3 * time.Second) // 3秒後にメッセージ送信
ch <- "hello"
}()
// 中断通知用ゴルーチン
go func() {
time.Sleep(1 * time.Second) // 1秒後に中断通知
done <- true
}()
select {
case msg := <-ch: // チャネルからの受信を待つ
fmt.Printf("送信成功 %s", msg)
case <-done: // 中断信号を受け取る
fmt.Println("処理中断")
}
}
// 出力:
// 処理中断
4. 無限ループでの使用
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
done := make(chan bool)
// 送信側ゴルーチン
go func() {
for i := 0; i < 3; i++ {
time.Sleep(1 * time.Second)
ch1 <- fmt.Sprintf("ch1: %d", i)
}
done <- true
}()
go func() {
for i := 0; i < 3; i++ {
time.Sleep(2 * time.Second)
ch2 <- fmt.Sprintf("ch2: %d", i)
}
done <- true
}()
// 受信側
finished := 0
for {
select {
case msg1 := <-ch1:
fmt.Println("受信1:", msg1)
case msg2 := <-ch2:
fmt.Println("受信2:", msg2)
case <-done:
finished++
if finished == 2 {
fmt.Println("全ての処理が完了")
return
}
}
}
}
// 出力:
// 受信1: ch1: 0
// 受信2: ch2: 0
// 受信1: ch1: 1
// 受信1: ch1: 2
// 受信2: ch2: 1
// 受信2: ch2: 2
// 全ての処理が完了
チャネルの注意点
- チャネルのクローズ
- 送信側が送信完了後にチャネルをクローズする
- クローズ済みのチャネルへの送信はパニックを引き起こす
- デッドロックの防止
- 送受信の数が合わない場合にデッドロックが発生
- バッファサイズの適切な設定が重要
- nil チャネル
- nil チャネルへの送受信は永久にブロックされる
- チャネルの初期化忘れに注意
ベストプラクティス
- チャネルは可能な限り細かいスコープで使用する
- 送信側でチャネルをクローズする
- 受信側でチャネルが閉じられているかチェックする
- バッファサイズは必要最小限に設定する
- エラー処理を適切に行う
まとめ
- チャネルはゴルーチン間でデータを安全に送受信するためのGoの並行処理機構
- 送受信操作はブロッキングを基本とし、ゴルーチン間の自動的な同期を実現
- バッファなしチャネルは 1 対 1 の同期通信、バッファありは指定容量まで非同期通信が可能
- チャネルは型安全で、コンパイル時に型チェックが行われる
close
関数でチャネルを閉じることができ、閉じた後は送信不可だが受信は可能select
文で複数のチャネル操作を同時に待ち受け、タイムアウトなどの制御が可能- デッドロック、nil チャネル、クローズ済みチャネルへの送信には注意が必要
- 適切なスコープ、バッファサイズ、エラー処理の実装がベストプラクティス
[Next] Step 2-6: バッファー(Buffer)
[Prev] Step 2-4: ゴルーチン(Goroutines)