Fix: Go fatal error: all goroutines are asleep - deadlock!
Part of: Go, Rust & Systems Errors
Quick Answer
How to fix Go fatal error all goroutines are asleep deadlock caused by unbuffered channels, missing goroutines, WaitGroup misuse, and channel direction errors.
The Error
Your Go program crashes with:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/app/main.go:8 +0x50Or variations:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/app/main.go:10 +0x68fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(...)Every goroutine in the program is blocked waiting for something (a channel operation, a mutex, a WaitGroup), and nothing can unblock any of them. Go’s runtime detects this condition and panics.
Why This Happens
Go detects deadlocks when all goroutines are blocked. The runtime checks if any goroutine can make progress. If none can, the program is deadlocked and cannot continue. This detection is performed by the runtime scheduler when it has no runnable goroutines left and every parked goroutine is waiting on a synchronization primitive that nothing can ever signal.
The detection is intentionally conservative. The runtime can only see goroutines that exist inside the current process. It cannot see file descriptors, network sockets, kernel timers, or anything outside the Go runtime. That is why a goroutine sitting inside time.Sleep, blocked on a network read, or waiting on a syscall does not count as deadlocked even when the rest of the program is hung — the runtime assumes those operations might eventually return. The deadlock check fires only when literally every goroutine is parked on a channel, mutex, or sync primitive.
Common causes:
- Sending to an unbuffered channel with no receiver. The sender blocks forever.
- Receiving from a channel with no sender. The receiver blocks forever.
- Forgetting to close a channel. A
rangeloop over a channel blocks forever waiting for more values. - WaitGroup counter never reaches zero.
wg.Wait()blocks becausewg.Done()is never called. - Mutex double-lock. Locking a mutex that is already locked by the same goroutine.
- Circular channel dependencies. Goroutine A waits on channel X, goroutine B waits on channel Y, and they need each other to proceed.
Version History That Changes the Failure Mode
Go’s scheduler and channel runtime have evolved in ways that change which deadlock-like bugs surface as the explicit fatal error panic versus silent hangs. Knowing which Go version you are on helps you interpret the symptom.
- Go 1.14 (Feb 2020) — asynchronous preemption. Before 1.14, tight loops without function calls could starve the scheduler. A goroutine spinning in a CPU-bound loop would never yield, so other goroutines never got to run. Some “deadlock-shaped” hangs in older code were actually starvation. From 1.14 onward, the runtime can preempt at safe points using signals, so a true all-goroutines-blocked condition is reported as a deadlock rather than disguised as starvation.
- Go 1.18 (Mar 2022) — generics. Generics changed how people write channel and worker-pool helpers. Type-parameterized channel utilities (
func Recv[T any](ch <-chan T) T) became common, and the olderinterface{}-based helpers gradually disappeared. The deadlock conditions did not change, but the call stacks in panic output now include generic instantiations likemain.send[int], which can be harder to read. - Go 1.20 (Feb 2023) —
errors.Join. Combined with the oldercontextcancellation patterns, this made it easier to thread cancellation through worker goroutines and surface the actual cause of a hang, instead of seeing only the goroutine that happened to be detected first. - Go 1.21 (Aug 2023) —
sync.OnceFunc,OnceValue,OnceValues. These helpers replaced a lot of hand-rolled mutex-and-flag initialization patterns. Code written before 1.21 still works, but porting it removes a common source of double-lock and partial-init deadlocks. - Go 1.22 (Feb 2024) — loop variable scoping change.
for i := range items { go func() { use(i) }() }now captures a freshiper iteration. Older code that relied on the old scoping behaviour (intentionally or not) may now finish faster or differently, occasionally exposing existing WaitGroup races that were previously masked. - Go 1.23+ (2024 onward) — improved runtime/trace and
GODEBUG=schedtrace. Tooling for diagnosing hangs improved. Usego tool traceon a recent toolchain to see which goroutines are blocked on which channel addresses.
The race detector (go run -race) has been part of Go since 1.1, but its output and overhead improved noticeably in 1.19 and 1.20. If you are on an older toolchain, upgrading just to run -race against suspect code is often worth it.
Fix 1: Fix Unbuffered Channel Sends
An unbuffered channel blocks the sender until a receiver is ready:
Broken — sending with no goroutine to receive:
func main() {
ch := make(chan int)
ch <- 42 // Deadlock! No goroutine is receiving
fmt.Println(<-ch)
}Fixed — receive in a goroutine:
func main() {
ch := make(chan int)
go func() {
ch <- 42 // Send in a goroutine
}()
fmt.Println(<-ch) // Receive in main
}Fixed — use a buffered channel:
func main() {
ch := make(chan int, 1) // Buffer size 1
ch <- 42 // Does not block (buffer has space)
fmt.Println(<-ch) // 42
}Pro Tip: Unbuffered channels (
make(chan T)) require both a sender and a receiver to be ready simultaneously. Buffered channels (make(chan T, N)) allow up to N sends without a receiver. Use unbuffered channels for synchronization and buffered channels for decoupling.
Fix 2: Fix Channel Range Loops
range over a channel blocks until the channel is closed:
Broken — channel never closed:
func main() {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// Forgot to close(ch)!
}()
for v := range ch { // Deadlock! range waits for close(ch) forever
fmt.Println(v)
}
}Fixed — close the channel when done sending:
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // Signal that no more values will be sent
}()
for v := range ch {
fmt.Println(v) // Prints 0-4, then exits the loop
}Fixed — use a known count instead of range:
for i := 0; i < 5; i++ {
fmt.Println(<-ch)
}Common Mistake: Closing a channel from the receiver side, or closing a channel multiple times. Only the sender should close a channel, and only close it once. Closing a closed channel causes a panic.
Fix 3: Fix WaitGroup Misuse
sync.WaitGroup deadlocks if Done() is never called:
Broken — Done() not called:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
// Forgot wg.Done()!
fmt.Println(n)
}(i)
}
wg.Wait() // Deadlock! Counter never reaches 0Fixed — use defer wg.Done():
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done() // Always called, even if the function panics
fmt.Println(n)
}(i)
}
wg.Wait()Broken — Add() called inside the goroutine (race condition):
for i := 0; i < 5; i++ {
go func(n int) {
wg.Add(1) // WRONG! Main goroutine might reach Wait() before Add()
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait() // Might return too earlyFixed — always call Add() before launching the goroutine:
for i := 0; i < 5; i++ {
wg.Add(1) // Add before starting the goroutine
go func(n int) {
defer wg.Done()
fmt.Println(n)
}(i)
}
wg.Wait()Fix 4: Fix Select with Default
Use select to avoid blocking on channel operations:
ch := make(chan int)
// Blocking receive (might deadlock)
value := <-ch
// Non-blocking receive with select
select {
case value := <-ch:
fmt.Println("Received:", value)
default:
fmt.Println("No value available")
}Timeout pattern:
select {
case value := <-ch:
fmt.Println("Received:", value)
case <-time.After(5 * time.Second):
fmt.Println("Timed out waiting for value")
}Multiple channels:
select {
case msg := <-msgCh:
handleMessage(msg)
case err := <-errCh:
handleError(err)
case <-ctx.Done():
fmt.Println("Context canceled")
return
}Fix 5: Fix Producer-Consumer Patterns
A common pattern that can deadlock if not implemented correctly:
Broken — single channel, single goroutine:
func main() {
jobs := make(chan int)
results := make(chan int)
// Producer
for i := 0; i < 5; i++ {
jobs <- i // Deadlock! No consumer running yet
}
close(jobs)
// Consumer
for j := range jobs {
results <- j * 2
}
}Fixed — start consumer first, or use goroutines:
func main() {
jobs := make(chan int, 10) // Buffered
results := make(chan int, 10)
// Start consumer goroutine first
go func() {
for j := range jobs {
results <- j * 2
}
close(results)
}()
// Producer
for i := 0; i < 5; i++ {
jobs <- i
}
close(jobs)
// Collect results
for r := range results {
fmt.Println(r)
}
}Worker pool pattern:
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
// Start 3 workers
for w := 0; w < 3; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs {
results <- j * 2
}
}()
}
// Send jobs
for i := 0; i < 10; i++ {
jobs <- i
}
close(jobs)
// Wait for workers and close results
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Println(r)
}
}Fix 6: Fix Mutex Deadlocks
Go’s sync.Mutex is not reentrant — locking it twice from the same goroutine deadlocks:
Broken:
var mu sync.Mutex
func doWork() {
mu.Lock()
defer mu.Unlock()
helper() // Calls Lock() again — deadlock!
}
func helper() {
mu.Lock() // Deadlock! Already locked by doWork()
defer mu.Unlock()
// ...
}Fixed — restructure to avoid nested locks:
func doWork() {
mu.Lock()
data := readData()
mu.Unlock()
result := processData(data) // No lock held during processing
mu.Lock()
writeResult(result)
mu.Unlock()
}Fixed — use a lock-free inner function:
func doWork() {
mu.Lock()
defer mu.Unlock()
helperLocked() // Assumes lock is already held
}
func helperLocked() {
// Does NOT lock — caller must hold the lock
// Document this requirement in a comment
}Fix 7: Fix Context Cancellation
Use context.Context for proper goroutine lifecycle management:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ch := make(chan string)
go func() {
result := longOperation()
ch <- result
}()
select {
case result := <-ch:
fmt.Println("Result:", result)
case <-ctx.Done():
fmt.Println("Operation timed out:", ctx.Err())
}
}Pass context to goroutines:
func worker(ctx context.Context, ch chan<- int) {
for i := 0; ; i++ {
select {
case <-ctx.Done():
return // Exit when context is canceled
case ch <- i:
time.Sleep(100 * time.Millisecond)
}
}
}Fix 8: Use the Race Detector
While the race detector does not detect deadlocks directly, it catches data races that often accompany deadlock-prone code:
go run -race main.go
go test -race ./...Debug with GOTRACEBACK:
GOTRACEBACK=all go run main.go
# Shows all goroutine stacks on deadlock, not just the relevant onesUse runtime.NumGoroutine() to monitor goroutine leaks:
fmt.Println("Goroutines:", runtime.NumGoroutine())Still Not Working?
Note: Go only detects deadlocks when all goroutines are blocked. If even one goroutine is running (e.g., a time.Sleep loop, an HTTP server), Go will not detect the deadlock. The program hangs silently instead of panicking.
Use pprof to debug hanging programs:
import _ "net/http/pprof"
go func() {
http.ListenAndServe(":6060", nil)
}()
// Visit http://localhost:6060/debug/pprof/goroutine?debug=2
// Shows all goroutine stacksCheck for nil channels. A nil channel blocks forever on both send and receive. This is sometimes used intentionally inside select to disable a case, but if you accidentally hit a nil channel outside a select, your goroutine parks and never wakes. Inspect any channel assigned from a function whose error path leaves the channel as the zero value.
Inspect the goroutine count over time. Add runtime.NumGoroutine() to a periodic log line. A steadily climbing number means goroutines are being created faster than they exit. Eventually one of them blocks the wrong primitive and the runtime reports a deadlock that is really a leak. See Fix: Go goroutine leak for the diagnosis pattern.
Check channel direction in function signatures. A function declared as func send(ch chan<- int) (send-only) cannot close the channel from inside, and a function declared as func recv(ch <-chan int) cannot send. Calling close() on a send-only parameter is a compile error, but more subtle direction confusions can lead to a channel that is never closed by anyone. Audit who owns the close.
Look for blocking calls inside a held mutex. A goroutine that holds mu.Lock() and then waits on a channel that another goroutine needs to take the same lock to write to is the textbook circular wait. The deadlock panic will name one of the goroutines, but the root cause is the lock-held-during-blocking-call pattern.
For Go index out of range panics, see Fix: Go panic: runtime error: index out of range. For concurrent map access that causes a different fatal error, see Fix: Go fatal error: concurrent map writes. For context-deadline patterns that interact with channel waits, see Fix: Go context deadline exceeded.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Go Test Not Working — Tests Not Running, Failing Unexpectedly, or Coverage Not Collected
How to fix Go testing issues — test function naming, table-driven tests, t.Run subtests, httptest, testify assertions, and common go test flag errors.
Fix: Go Generics Type Constraint Error — Does Not Implement or Cannot Use as Type
How to fix Go generics errors — type constraints, interface vs constraint, comparable, union types, type inference failures, and common generic function pitfalls.
Fix: Go Deadlock — all goroutines are asleep, deadlock!
How to fix Go channel deadlocks — unbuffered vs buffered channels, missing goroutines, select statements, closing channels, sync primitives, and detecting deadlocks with go race detector.
Fix: Go Error Handling Not Working — errors.Is, errors.As, and Wrapping
How to fix Go error handling — errors.Is vs ==, errors.As for type extraction, fmt.Errorf %w for wrapping, sentinel errors, custom error types, and stack traces.