エラーの内容
プログラムが以下のエラーでクラッシュします:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/home/user/project/main.go:12 +0x58
exit status 2
Goのランタイムがすべてのゴルーチンがブロックされていることを検出しました — 処理が一切前に進めない状態です。そのため、プログラムを永遠にハングさせる代わりに強制終了します。これはGoがあなたに代わって対処してくれていると考えてください。
原因
デッドロックは、すべてのゴルーチンが永遠に届かないものを待ち続けているときに発生します。対応するゴルーチンのないチャネル。同じゴルーチンが再度ロックしようとしているミューテックス。カウンターが決してゼロにならないWaitGroupなどが原因です。
よくある原因:
- nilチャネルへの送受信
- 誰も書き込まないチャネルへの待機(またはその逆)
- ゴルーチン内での
wg.Done()の忘れ - 同じゴルーチンから非再入可能なミューテックスを2回ロックする
- 送信側と受信側が同じゴルーチン内にあるアンバッファードチャネル
修正手順
1. 受信側のない送信(または送信側のない受信)
これは誰もが一度は経験するパターンです。チャネルに書き込んでいるのに誰も読み取っていない、またはチャネルがnilの場合です:
// 悪い例: chを読み取るゴルーチンがない
ch := make(chan int)
ch <- 42 // 永遠にブロック → デッドロック
fmt.Println(<-ch)
修正方法:送信側と受信側を別々のゴルーチンで実行する必要があります。
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch) // ゴルーチンが送信するとブロック解除
受信が後になり、値を1つバッファリングするだけでよい場合は、バッファードチャネルも使えます:
ch := make(chan int, 1) // 容量1
ch <- 42 // 即座に返る
fmt.Println(<-ch)
2. チャネルのクローズ忘れ(rangeループが止まらない)
// 悪い例
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// 忘れ: close(ch)
}()
for v := range ch { // 0〜4を受信後、永遠に待機
fmt.Println(v)
}
チャネルへのrangeはチャネルがクローズされるまで続きます。close(ch)がなければ、ループは終わりません。
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // rangeに完了を通知
}()
3. WaitGroupのカウンターがゼロにならない
// 悪い例: processItemがパニックや早期リターンした場合、Doneが呼ばれない
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
processItem(n)
// wg.Done()を忘れている
}(i)
}
wg.Wait() // デッドロック
ゴルーチンの先頭でdefer wg.Done()を使いましょう。関数がパニックしても必ず実行されます:
go func(n int) {
defer wg.Done() // 必ず実行される
processItem(n)
}(i)
4. 同一ゴルーチン内で同じミューテックスを2回ロックする
sync.Mutexは再入可能ではありません。同じゴルーチンから2回ロックするとハングします:
// 悪い例
var mu sync.Mutex
func doWork() {
mu.Lock()
defer mu.Unlock()
doMoreWork() // これもmu.Lock()を呼ぶ → デッドロック
}
func doMoreWork() {
mu.Lock() // 同じゴルーチンがすでにロックを保持している
defer mu.Unlock()
}
内部関数はロックがすでに保持されていることを前提とするように再構成し、Lock()を呼ばせないようにします:
func doWork() {
mu.Lock()
defer mu.Unlock()
doMoreWorkLocked() // 慣習: "Locked"サフィックス = 呼び出し元がロックを保持
}
func doMoreWorkLocked() {
// ここではmu.Lock()を呼ばない
}
5. nilチャネル
nilチャネルへの送受信は永遠にブロックされます — パニックにならず、ただ無音のまま止まります:
// 悪い例
var ch chan int // nil
ch <- 1 // 永遠にブロック
必ずmakeで初期化してください:
ch := make(chan int) // アンバッファード
ch := make(chan int, 10) // バッファード、容量10
ゴルーチンスタックダンプの読み方
デッドロックが発生すると、Goはゴルーチンの完全なダンプを出力します。読み飛ばさないでください — 各ゴルーチンがどこで止まっているかを正確に教えてくれます:
goroutine 1 [chan receive]:
main.main()
/home/user/project/main.go:12 +0x58
[chan receive] — チャネルからの受信待ちでブロックされています。
[semacquire] — ミューテックスのロック待ちでブロックされています。
[sleep] — time.Sleepの中にいます(これ単体ではデッドロックしません)。
状態タグと行番号をコードと照合してください — そこがデッドロックの発生箇所です。
また、データ競合とデッドロックはセットで現れることが多いため、レースディテクターを使って実行することも有効です:
go run -race main.go
go test -race ./...
修正の確認
正常終了すればデッドロックは解消されています:
# 終了コード0で終了し、パニック出力がないこと
go run main.go
# テスト + レース検出
go test -race ./...
# 長時間稼働するプログラムの場合、pprofでゴルーチンリークを確認
import _ "net/http/pprof"
// curl http://localhost:6060/debug/pprof/goroutine?debug=1
出力にfatal errorがなく、終了コードが0であれば問題ありません。
確認チェックリスト
- すべてのチャネル送信に対して、別のゴルーチンで受信側が動いている
rangeで使われるチャネルはプロデューサーがclose()している- すべての
wg.Add(1)に対応するdefer wg.Done()がある - 同じ
sync.Mutexを1つのゴルーチンが2回ロックしていない - 送受信時にチャネル変数がnilになっていない
defaultケースのないselectがすべてのゴルーチンを同時にブロックしていない
ヒント
- Goのデッドロック検出器はすべてのゴルーチンがブロックされた場合のみ発火します。1つでも実行中のゴルーチンがある場合(例:スピナーループ)、ランタイムは検出しません。その場合は
pprofを使ってゴルーチンリークを検出してください。 - キャンセル処理には、独自のチャネルプロトコルを作るよりもタイムアウト付きの
context.Contextを使いましょう。コードがすっきりし、ゴルーチンが静かにリークするのを防げます。 - テストスイートに
goleak(github.com/uber-go/goleak)を追加しましょう。テスト終了時にゴルーチンが残っていた場合にテストを失敗させてくれるため、本番環境に入る前にリークを検出できます。

