こんにちは、かつコーチです。
前回はgoroutineの基本を解説しました。
goroutineは便利な反面、そのままでは複数の歯車がバラバラに動くだけで、お互いにデータをやり取りできません。
そこで登場するのがchannelです。
この記事では、channelの基本的な使い方と、私が実際にハマった「デッドロック」というエラーについて解説します。
channelとは?
channelの定義
channelとは、goroutine同士をつなぐベルトコンベアのようなものです。
技術的に言うと、channelは複数のgoroutineの間で安全にデータを受け渡しするための通路です。
Goの公式な考え方には「メモリを共有して通信するな、通信することでメモリを共有せよ」という言葉があります。
これは、複数の歯車(goroutine)が同じ荷物を直接奪い合うのではなく、ベルトコンベア(channel)を介して荷物を受け渡しすることで、安全にデータをやり取りしよう、という思想です。
なぜchannelが必要なのか
複数のgoroutineが同じ変数を直接読み書きすると、思わぬタイミングで値が壊れたり、予測できない結果になったりします。
channelを使えば、「この歯車の作業が終わったらベルトコンベアに荷物を乗せる」「別の歯車はベルトコンベアから荷物が流れてくるのを待つ」という形で、安全にデータを受け渡せます。
これにより、複数のgoroutineが同時に同じデータを触ってしまう不具合を防ぐことができます。
基本の書き方
手順1: make(chan T)でchannelを作る
channelはmake関数を使って作成します。
chan TのTの部分に、やり取りしたいデータの型を指定します。
package main
import "fmt"
func main() {
ch := make(chan string) // string型を流すchannel
fmt.Println(ch)
}
手順2: 送信と受信の基本構文
channelへの送信はch <- 値、受信は<-chという構文を使います。
矢印の向きが、ベルトコンベアの上を荷物が流れる向きだとイメージすると覚えやすいです。
package main
import "fmt"
func sendMessage(ch chan string) {
ch <- "こんにちは、goroutineから!" // channelに送信
}
func main() {
ch := make(chan string)
go sendMessage(ch)
message := <-ch // channelから受信
fmt.Println(message)
}
このコードでは、sendMessageというgoroutineがベルトコンベアに荷物(文字列)を乗せ、main関数がその荷物を受け取っています。
手順3: 複数の値をやり取りする
forループとchannelを組み合わせて、複数の値を順番に受け渡すこともできます。
package main
import "fmt"
func generateNumbers(ch chan int) {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch) // 送信が終わったらchannelを閉じる
}
func main() {
ch := make(chan int)
go generateNumbers(ch)
// closeされるまでrangeで受信し続ける
for num := range ch {
fmt.Println("受信:", num)
}
}
close(ch)でchannelを閉じることで、受信側は「もうこれ以上データが流れてこない」と判断でき、rangeループが自然に終了します。
つまずきやすい設定・注意点
make(chan T)だけで作ったchannelはバッファなしチャネル(容量0のchannel。送信側と受信側が同時に準備できて初めてデータが流れる)です。
バッファなしチャネルは、送信と受信が同時に成立しないと処理が先に進みません。
この性質を理解していないと、次に紹介するデッドロックにすぐハマります。
よくあるつまずきポイント・エラー対処
all goroutines are asleep – deadlock!で止まった話
私が実際にchannelを使い始めたころ、真っ先にぶつかったのがこのエラーでした。
❌ Before
package main
import "fmt"
func main() {
ch := make(chan string)
ch <- "こんにちは" // 受信するgoroutineがいない
message := <-ch
fmt.Println(message)
}
このコードを実行すると、次のようなエラーでプログラムが止まりました。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/path/to/main.go:7 +0x25
私は最初、このエラーメッセージの意味が全く分かりませんでした。
「goroutineなんて1つも作ってないのに、なんでgoroutineの話が出てくるんだ」と戸惑ったのを覚えています。
調べて分かったのは、main関数自体も1つのgoroutine(メインgoroutine)として扱われる、ということでした。
原因はシンプルで、バッファなしチャネルへの送信ch <- "こんにちは"は、誰かが同時に受信してくれるまで処理がそこで止まってしまいます。
ところがこのコードでは、送信した直後の行で自分自身が受信しようとしているため、送信の時点でずっと待ち状態になり、受信する行には永遠にたどり着けません。
つまり「送信を待つgoroutineしか存在しない」状態になり、Goのランタイムが「これ以上誰も動けない」と判断してプログラムごと強制終了させたのです。
✅ After
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "こんにちは" // 別のgoroutineが送信を担当
}()
message := <-ch // main関数は受信に専念する
fmt.Println(message)
}
送信を別のgoroutineに任せることで、main関数は安心して受信を待てるようになり、デッドロックが解消しました。
この経験から、私は「バッファなしチャネルの送受信は、必ず別々のgoroutineで行う」ことを徹底するようになりました。
バッファ付きチャネルとの混同
もう1つよくある勘違いが、バッファ付きチャネルなら何でも即座に送受信できると思い込んでしまうことです。
❌ Before
ch := make(chan int, 2) // バッファ2のchannel
ch <- 1
ch <- 2
ch <- 3 // バッファが満杯でここで止まる
✅ After
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println(<-ch) // 1つ受信してバッファに空きを作る
ch <- 3 // これで送信できる
make(chan int, 2)のようにバッファサイズを指定すると、その数までは受信を待たずに送信できます。
ただし、バッファが満杯になれば、やはり誰かが受信するまで送信は止まります。
応用・一歩先の使い方
複数のchannelを同時に監視したい場合は、select文を使います。
これは、複数のベルトコンベアを見張っておいて、先に荷物が流れてきた方を処理する、というイメージです。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "ch1からのメッセージ"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "ch2からのメッセージ"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
selectを使いこなせるようになると、より柔軟な並行処理のパターンが組めるようになります。
まとめ
この記事のポイント
- channelはgoroutine同士をつなぐベルトコンベアのような仕組み
make(chan T)でchannelを作り、ch <- 値で送信、<-chで受信する- バッファなしチャネルは送受信が同時に成立しないと処理が進まない
- 送受信を同じgoroutine内で完結させようとすると
all goroutines are asleep - deadlock!が発生する - 複数channelを扱うときは
select文が便利
次に読むべき記事
goroutineとchannelの基本を押さえたら、次は「sync.WaitGroupとMutexで安全に並行処理する」に進んでみてください。
channelとMutexの使い分けについても解説しています。