Introduction
An unbuffered channel in Go blocks the sender until a receiver is ready. If no goroutine is receiving from the channel, the sender blocks forever, causing a deadlock. Go's runtime detects this and panics with "all goroutines are asleep - deadlock!".
This is a fundamental concurrency error in Go that occurs when goroutine lifecycles are not properly coordinated.
Symptoms
- Application panics with "fatal error: all goroutines are asleep - deadlock!"
- Goroutine hangs indefinitely with no error output (when runtime doesn't detect the deadlock)
- Program works with small inputs but deadlocks with larger data sets
Common Causes
- Sending to an unbuffered channel when no goroutine is receiving
- Receiver goroutine exited before the sender completed
- Channel created but the receiver was never started
Step-by-Step Fix
- 1.Use buffered channels when sender may outpace receiver: Buffered channels allow sending without an immediate receiver.
- 2.```go
- 3.// BAD: unbuffered channel deadlocks if no receiver
- 4.ch := make(chan int)
- 5.ch <- 42 // Blocks forever if nobody receives
// GOOD: buffered channel allows sends up to capacity ch := make(chan int, 100) for i := 0; i < 50; i++ { ch <- i // Won't block until buffer is full } ```
- 1.Use select with timeout for safe sends: Prevent indefinite blocking with a timeout case.
- 2.```go
- 3.func sendWithTimeout(ch chan int, value int, timeout time.Duration) error {
- 4.select {
- 5.case ch <- value:
- 6.return nil
- 7.case <-time.After(timeout):
- 8.return fmt.Errorf("send timed out after %v", timeout)
- 9.}
- 10.}
// Usage: err := sendWithTimeout(ch, 42, 5*time.Second) if err != nil { log.Printf("Could not send: %v", err) } ```
- 1.Ensure receiver goroutine is started before sending: Start the consumer before the producer.
- 2.```go
- 3.func main() {
- 4.jobs := make(chan int, 100)
- 5.results := make(chan int, 100)
// Start receiver FIRST done := make(chan struct{}) go func() { for r := range results { fmt.Println("Result:", r) } close(done) }()
// Then send for i := 0; i < 10; i++ { jobs <- i } close(jobs)
<-done } ```
- 1.Use non-blocking send with default case: Skip the send if no receiver is ready.
- 2.```go
- 3.// Non-blocking send:
- 4.select {
- 5.case ch <- value:
- 6.fmt.Println("Sent", value)
- 7.default:
- 8.fmt.Println("Channel full or no receiver, skipping")
- 9.}
// Non-blocking receive: select { case val := <-ch: fmt.Println("Received", val) default: fmt.Println("No value available") } ```
Prevention
- Use buffered channels when the producer-consumer rate is unbalanced
- Always use select with timeout for channel operations that could block
- Run go test with race detector to catch channel issues
- Document channel ownership: which goroutine creates, sends, receives, and closes