The Error
Your program crashes with:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/home/user/project/main.go:12 +0x58
exit status 2
Go's runtime detected that every goroutine is blocked โ nothing can move forward. So it kills the program instead of hanging forever. Think of it as Go doing you a favor.
Why This Happens
A deadlock occurs when all goroutines are stuck waiting on something that will never arrive. A channel with no counterpart. A mutex locked by the same goroutine trying to lock it again. A WaitGroup whose counter will never hit zero.
The usual suspects:
- Sending to or receiving from a nil channel
- Waiting on a channel that nobody writes to (or vice versa)
- Forgetting
wg.Done()inside a goroutine - Locking a non-reentrant mutex twice from the same goroutine
- Unbuffered channel where sender and receiver are both in the same goroutine
Step-by-Step Fixes
1. Send with no receiver (or receive with no sender)
This one catches everyone eventually. You write to a channel but nobody's reading from it โ or the channel is nil:
// BAD: no goroutine is reading from ch
ch := make(chan int)
ch <- 42 // blocks forever โ deadlock
fmt.Println(<-ch)
The fix: sender and receiver must run in separate goroutines.
ch := make(chan int)
go func() {
ch <- 42
}()
fmt.Println(<-ch) // unblocks once the goroutine sends
If the receiver comes later and you just need to buffer one value, a buffered channel works too:
ch := make(chan int, 1) // capacity of 1
ch <- 42 // returns immediately
fmt.Println(<-ch)
2. Forgetting to close a channel (range loop hangs)
// BAD
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// forgot: close(ch)
}()
for v := range ch { // receives 0โ4, then waits forever
fmt.Println(v)
}
A range over a channel keeps going until the channel is closed. No close(ch) โ the loop never ends.
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
close(ch) // tells range: we're done
}()
3. WaitGroup counter never reaches zero
// BAD: if processItem panics or returns early, Done never fires
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
processItem(n)
// forgot wg.Done()
}(i)
}
wg.Wait() // deadlock
Use defer wg.Done() as the very first line inside the goroutine. It runs even if the function panics:
go func(n int) {
defer wg.Done() // guaranteed to fire
processItem(n)
}(i)
4. Locking the same mutex twice in one goroutine
sync.Mutex is not reentrant. Lock it twice from the same goroutine and you'll hang:
// BAD
var mu sync.Mutex
func doWork() {
mu.Lock()
defer mu.Unlock()
doMoreWork() // this also calls mu.Lock() โ deadlock
}
func doMoreWork() {
mu.Lock() // same goroutine already holds the lock
defer mu.Unlock()
}
Restructure so the inner function assumes the lock is already held โ don't let it call Lock() itself:
func doWork() {
mu.Lock()
defer mu.Unlock()
doMoreWorkLocked() // convention: "Locked" suffix = caller owns the lock
}
func doMoreWorkLocked() {
// no mu.Lock() here
}
5. Nil channel
Sending to or receiving from a nil channel blocks forever โ no panic, just silence:
// BAD
var ch chan int // nil
ch <- 1 // blocks forever
Always initialize with make:
ch := make(chan int) // unbuffered
ch := make(chan int, 10) // buffered, capacity 10
Reading the Goroutine Stack Dump
When the deadlock fires, Go prints a full goroutine dump. Don't skip it โ it tells you exactly where each goroutine is stuck:
goroutine 1 [chan receive]:
main.main()
/home/user/project/main.go:12 +0x58
[chan receive] โ blocked waiting to receive from a channel.
[semacquire] โ blocked on a mutex lock.
[sleep] โ inside time.Sleep (won't deadlock on its own).
Match the state tag and line number to your code โ that's your deadlock site.
Also worth running with the race detector, since data races and deadlocks often show up together:
go run -race main.go
go test -race ./...
Verify the Fix
Clean exit means the deadlock is gone:
# Should exit with code 0, no panic output
go run main.go
# Tests + race detection
go test -race ./...
# For long-running programs, check for goroutine leaks via pprof
import _ "net/http/pprof"
// curl http://localhost:6060/debug/pprof/goroutine?debug=1
No fatal error in the output and exit code 0 โ you're good.
Quick Checklist
- Every channel send has a receiver running in a different goroutine
- Channels used with
rangeareclose()d by the producer - Every
wg.Add(1)is paired with adefer wg.Done() - No goroutine locks the same
sync.Mutextwice - No channel variable is nil at send/receive time
- No
selectwithout adefaultcase blocks all goroutines at once
Tips
- Go's deadlock detector only fires when all goroutines are blocked. If even one goroutine is still running (say, a spinner loop), the runtime won't catch it. Use
pprofto spot goroutine leaks in that case. - For cancellation logic, reach for
context.Contextwith a timeout rather than rolling your own channel protocol. It's cleaner and prevents goroutines from leaking silently. - Add
goleak(github.com/uber-go/goleak) to your test suite. It fails the test if any goroutine is still running when the test exits โ catches leaks before they hit production.

