deadlock — all goroutines are asleep em Go

O erro “fatal error: all goroutines are asleep - deadlock!” é um panic do runtime do Go que acontece quando todas as goroutines do programa estão bloqueadas esperando por algo que nunca vai acontecer. O runtime detecta que nenhuma goroutine pode prosseguir e encerra o programa.

Este é um dos erros mais comuns ao trabalhar com concorrência em Go, especialmente com channels e mutexes.


A Mensagem de Erro

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
    /app/main.go:8 +0x34
exit status 2

Causas Comuns

1. Channel Sem Buffer — Leitura sem Goroutine para Escrever

Um channel sem buffer bloqueia até que haja alguém do outro lado:

package main

func main() {
    ch := make(chan int) // Channel sem buffer

    // DEADLOCK: main espera para sempre — ninguém envia
    valor := <-ch
    _ = valor
}

2. Channel Sem Buffer — Escrita sem Goroutine para Ler

O inverso também causa deadlock:

package main

func main() {
    ch := make(chan int)

    // DEADLOCK: main bloqueia tentando enviar
    // (ninguém está lendo do outro lado)
    ch <- 42
}

3. WaitGroup Mal Configurado

Esquecer de chamar Done() ou chamar Add() incorretamente:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(2) // Espera 2 Done()

    go func() {
        defer wg.Done()
        fmt.Println("goroutine 1")
    }()

    // Esqueceu a segunda goroutine!
    // Só 1 Done() será chamado, mas Add(2) espera 2

    // DEADLOCK: wg.Wait() espera para sempre
    wg.Wait()
}

4. Mutex Reentrante (Recursive Lock)

Go não suporta mutex reentrante. Tentar bloquear um mutex já bloqueado pela mesma goroutine causa deadlock:

package main

import (
    "fmt"
    "sync"
)

var mu sync.Mutex

func funcA() {
    mu.Lock()
    defer mu.Unlock()
    fmt.Println("funcA")
    funcB() // Chama funcB que também tenta bloquear mu
}

func funcB() {
    mu.Lock() // DEADLOCK: mu já está bloqueado por funcA na mesma goroutine
    defer mu.Unlock()
    fmt.Println("funcB")
}

func main() {
    funcA()
}

5. Channels Cruzados

Duas goroutines esperando uma pela outra:

package main

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        valor := <-ch1 // Espera ch1...
        ch2 <- valor   // ...depois envia em ch2
    }()

    go func() {
        valor := <-ch2 // Espera ch2...
        ch1 <- valor   // ...depois envia em ch1
    }()

    // DEADLOCK: goroutine 1 espera ch1, goroutine 2 espera ch2
    // Ninguém envia primeiro!
    select {}
}

Como Resolver

Solução 1: Usar Goroutine para Operações de Channel

Sempre tenha um produtor e consumidor em goroutines separadas:

package main

import "fmt"

func main() {
    ch := make(chan int)

    // Goroutine produtora
    go func() {
        ch <- 42
    }()

    // Main consome
    valor := <-ch
    fmt.Println(valor) // 42
}

Solução 2: Channels com Buffer

Channels com buffer não bloqueiam enquanto houver espaço:

package main

import "fmt"

func main() {
    ch := make(chan int, 1) // Buffer de 1

    ch <- 42          // Não bloqueia (buffer tem espaço)
    valor := <-ch     // Lê do buffer
    fmt.Println(valor) // 42
}

Solução 3: Fechar Channels Quando Terminar

Use close() para sinalizar que não há mais dados:

package main

import "fmt"

func gerarNumeros(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // Sinaliza fim
}

func main() {
    ch := make(chan int)

    go gerarNumeros(ch)

    // range termina quando o channel é fechado
    for num := range ch {
        fmt.Println(num)
    }
}

Solução 4: Corrigir WaitGroup

Garanta que Add e Done estejam balanceados:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    tarefas := []string{"A", "B", "C"}

    for _, tarefa := range tarefas {
        wg.Add(1)
        go func(t string) {
            defer wg.Done()
            fmt.Println("Processando:", t)
        }(tarefa)
    }

    wg.Wait() // Funciona: cada goroutine chama Done()
    fmt.Println("Todas as tarefas concluídas")
}

Solução 5: Usar select com Timeout

Evite bloqueios infinitos com select e time.After:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)

    go func() {
        time.Sleep(5 * time.Second) // Simula operação lenta
        ch <- 42
    }()

    select {
    case valor := <-ch:
        fmt.Println("Recebido:", valor)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout: operação demorou demais")
    }
}

Para timeouts mais sofisticados, use o pacote context.


Quando o Runtime NÃO Detecta Deadlock

O runtime do Go só detecta deadlock quando todas as goroutines estão bloqueadas. Se uma goroutine continua executando (mesmo que não contribua), o deadlock parcial passa despercebido:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int)

    // Esta goroutine nunca envia em ch
    go func() {
        for {
            time.Sleep(time.Second)
            fmt.Println("estou viva!")
        }
    }()

    // Bloqueia para sempre, mas o runtime NÃO detecta deadlock
    // porque a outra goroutine está ativa
    <-ch
}

Para detectar esses casos, use profiling e monitore goroutines bloqueadas com pprof.


Dicas para Evitar Deadlocks

  1. Sempre feche channels — quando o produtor terminar, use close(ch). Leia sobre padrões de concorrência.

  2. Use select com default ou timeout — nunca bloqueie indefinidamente sem escape.

  3. Evite mutex reentrante — reestruture o código para que a mesma goroutine não tente bloquear o mesmo mutex duas vezes.

  4. Use o race detectorgo run -race . detecta condições de corrida que podem levar a deadlocks. Veja data race detected.

  5. Prefira channels a mutexes — channels comunicam intenção mais claramente. Consulte concorrência em Go.

  6. Teste com -count e -race — testes de concorrência devem rodar múltiplas vezes para expor race conditions: go test -race -count=100.

Para comparação, Rust previne deadlocks e data races em tempo de compilação através do sistema de ownership e borrow checker — uma abordagem fundamentalmente diferente da detecção em runtime do Go.


Erros Relacionados