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. 1.Use buffered channels when sender may outpace receiver: Buffered channels allow sending without an immediate receiver.
  2. 2.```go
  3. 3.// BAD: unbuffered channel deadlocks if no receiver
  4. 4.ch := make(chan int)
  5. 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. 1.Use select with timeout for safe sends: Prevent indefinite blocking with a timeout case.
  2. 2.```go
  3. 3.func sendWithTimeout(ch chan int, value int, timeout time.Duration) error {
  4. 4.select {
  5. 5.case ch <- value:
  6. 6.return nil
  7. 7.case <-time.After(timeout):
  8. 8.return fmt.Errorf("send timed out after %v", timeout)
  9. 9.}
  10. 10.}

// Usage: err := sendWithTimeout(ch, 42, 5*time.Second) if err != nil { log.Printf("Could not send: %v", err) } ```

  1. 1.Ensure receiver goroutine is started before sending: Start the consumer before the producer.
  2. 2.```go
  3. 3.func main() {
  4. 4.jobs := make(chan int, 100)
  5. 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. 1.Use non-blocking send with default case: Skip the send if no receiver is ready.
  2. 2.```go
  3. 3.// Non-blocking send:
  4. 4.select {
  5. 5.case ch <- value:
  6. 6.fmt.Println("Sent", value)
  7. 7.default:
  8. 8.fmt.Println("Channel full or no receiver, skipping")
  9. 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