O que é Recover em Go?

A função built-in recover em Go permite interceptar um panic em andamento e retomar o controle da execução do programa. Sem recover, um panic interrompe o programa completamente. Com recover, você pode capturar o panic, tratar o erro, e continuar executando normalmente.

Recover é parte do trio panic/defer/recover — o mecanismo de Go para lidar com situações excepcionais. Enquanto error é o sistema principal de tratamento de erros em Go, recover existe para aqueles casos raros onde um panic precisa ser capturado e convertido em um error controlado.

Regra fundamental

recover() só funciona quando chamado dentro de uma função defer. Chamado em qualquer outro contexto, recover simplesmente retorna nil e não tem nenhum efeito:

package main

import "fmt"

func main() {
    // ERRADO: recover fora de defer não faz nada
    r := recover()
    fmt.Println("Recover fora de defer:", r) // nil

    // CORRETO: recover dentro de defer
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Panic recuperado:", r)
        }
    }()

    panic("algo deu errado!")
}

Saída:

Recover fora de defer: <nil>
Panic recuperado: algo deu errado!

O Padrão Defer/Recover

O padrão canônico para capturar panics em Go combina defer com recover de forma idiomática. A estrutura é sempre a mesma: uma função anônima deferida que chama recover e trata o resultado.

func operacaoSegura() (err error) {
    defer func() {
        if r := recover(); r != nil {
            // Converte o panic em um error
            switch v := r.(type) {
            case error:
                err = fmt.Errorf("panic recuperado: %w", v)
            case string:
                err = fmt.Errorf("panic recuperado: %s", v)
            default:
                err = fmt.Errorf("panic recuperado: %v", v)
            }
        }
    }()

    // Código que pode entrar em pânico
    executarOperacaoArriscada()
    return nil
}

Observe que a função usa named return values (err error) para que a função deferida possa modificar o valor de retorno. Isso é essencial para o padrão funcionar corretamente — sem named returns, o caller receberia o valor zero de error (nil).

Recover em HTTP Handlers

Um dos usos mais comuns de recover é em servidores HTTP. Sem recover, um panic em qualquer handler derrubaria todo o servidor. O pacote net/http da biblioteca padrão já inclui um recover básico, mas é boa prática adicionar o seu próprio middleware de recuperação:

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                // Log do panic com stack trace
                stackTrace := debug.Stack()
                log.Printf(
                    "PANIC: %v\nMethod: %s\nURL: %s\nStack:\n%s",
                    rec, r.Method, r.URL.Path, stackTrace,
                )

                // Retorna 500 para o cliente
                http.Error(w, "Erro interno do servidor", http.StatusInternalServerError)
            }
        }()

        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/dados", handleDados)

    // Envolve todos os handlers com recovery
    servidor := recoveryMiddleware(mux)
    log.Fatal(http.ListenAndServe(":8080", servidor))
}

Frameworks populares como Gin, Echo e Chi já incluem middleware de recovery por padrão. Mas entender como implementar o seu próprio é fundamental para casos customizados.

Recover em Goroutines

Um panic em uma goroutine que não é recuperado derruba todo o programa. Por isso, é prática essencial proteger goroutines com recover, especialmente em workers de longa duração:

func iniciarWorkers(numWorkers int, tarefas <-chan Tarefa) {
    for i := 0; i < numWorkers; i++ {
        go func(id int) {
            for tarefa := range tarefas {
                executarComRecovery(id, tarefa)
            }
        }(i)
    }
}

func executarComRecovery(workerID int, tarefa Tarefa) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf(
                "Worker %d: panic ao processar tarefa %s: %v\nStack: %s",
                workerID, tarefa.ID, r, debug.Stack(),
            )
            // Worker continua processando próxima tarefa
        }
    }()

    tarefa.Executar()
}

Sem esse recover, um único panic em uma tarefa derrubaria todo o pool de workers e, consequentemente, todo o programa. Com recover, o worker registra o erro e continua processando a próxima tarefa.

Padrão seguro para lançar goroutines

Crie um helper reutilizável para goroutines protegidas:

// SafeGo lança uma goroutine com recover automático
func SafeGo(fn func()) {
    go func() {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("Goroutine panic: %v\n%s", r, debug.Stack())
            }
        }()
        fn()
    }()
}

// Uso
func main() {
    SafeGo(func() {
        // código que pode entrar em pânico
        processarDados()
    })
}

Esse padrão pode ser estendido para incluir métricas, alertas, ou retry automático usando context para cancelamento.

Logging de Panics Recuperados

Simplesmente recuperar um panic sem registrar informações suficientes é um antipadrão perigoso. Sempre faça log detalhado de panics recuperados:

func logRecover(contexto string) {
    if r := recover(); r != nil {
        // Captura stack trace
        stack := debug.Stack()

        // Log estruturado com slog (Go 1.21+)
        slog.Error("Panic recuperado",
            "contexto", contexto,
            "panic", fmt.Sprintf("%v", r),
            "stack", string(stack),
        )

        // Ou com log tradicional
        log.Printf("[PANIC] Contexto: %s | Valor: %v | Stack:\n%s",
            contexto, r, stack)
    }
}

func processarPedido(pedidoID string) {
    defer logRecover(fmt.Sprintf("processamento do pedido %s", pedidoID))

    // ... processamento ...
}

Para sistemas de produção, considere enviar panics recuperados para um sistema de monitoramento como Sentry ou Datadog. O pacote log da biblioteca padrão e o mais recente slog facilitam essa integração.

Convertendo Panic em Error

O padrão mais robusto converte panics em errors tipados, permitindo que o caller trate o erro de forma idiomática:

// PanicError representa um panic recuperado como error
type PanicError struct {
    Value interface{}
    Stack []byte
}

func (e *PanicError) Error() string {
    return fmt.Sprintf("panic recuperado: %v", e.Value)
}

// WithRecover executa fn e converte qualquer panic em error
func WithRecover(fn func() error) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = &PanicError{
                Value: r,
                Stack: debug.Stack(),
            }
        }
    }()
    return fn()
}

// Uso
func main() {
    err := WithRecover(func() error {
        // código que pode dar panic
        dados := carregarDados()
        return processarDados(dados)
    })

    if err != nil {
        var pe *PanicError
        if errors.As(err, &pe) {
            log.Printf("Panic recuperado: %v\nStack: %s", pe.Value, pe.Stack)
        } else {
            log.Printf("Erro normal: %v", err)
        }
    }
}

Esse padrão usa a interface error e o sistema de wrapping de erros do Go para integrar panics recuperados ao fluxo normal de tratamento de erros.

Quando NÃO Usar Recover

Recover é uma ferramenta poderosa, mas seu uso excessivo pode mascarar bugs sérios. Existem situações onde você não deve usar recover:

  1. Não use recover para esconder bugs: Se o código entra em panic por um bug, corrija o bug em vez de silenciar o panic.

  2. Não use recover como try/catch genérico: Go não tem exceções por design. Usar panic/recover como substituto de try/catch vai contra a filosofia da linguagem.

  3. Não recupere panics do runtime sem ação: Panics como nil pointer dereference ou slice index out of bounds indicam bugs que precisam ser corrigidos.

  4. Não use recover em main sem re-panic: Se main recupera todos os panics silenciosamente, você perde visibilidade sobre bugs críticos.

// ANTIPADRÃO: engolir panics silenciosamente
func ruim() {
    defer func() {
        recover() // silencia tudo — bugs ficam invisíveis
    }()
    // ...
}

// CORRETO: recuperar, logar, e possivelmente re-panic
func bom() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Panic: %v\n%s", r, debug.Stack())
            // Dependendo do caso, pode re-panic:
            // panic(r)
        }
    }()
    // ...
}

Recover e Testing

Nos testing em Go, você pode verificar se uma func entra em panic conforme esperado:

func TestDevePanicComValorNegativo(t *testing.T) {
    defer func() {
        r := recover()
        if r == nil {
            t.Error("Esperava panic, mas não ocorreu")
        }
        msg, ok := r.(string)
        if !ok || msg != "valor negativo não permitido" {
            t.Errorf("Mensagem de panic inesperada: %v", r)
        }
    }()

    calcular(-1) // deve entrar em panic
}

Bibliotecas como testify oferecem helpers como assert.Panics() e assert.PanicsWithValue() que simplificam esses testes.

Próximos Passos

Para aprofundar seus conhecimentos sobre tratamento de erros e recuperação em Go, explore:


Perguntas Frequentes

Por que recover só funciona dentro de defer?

Recover precisa ser chamado durante o “unwinding” da pilha causado pelo panic. Funções defer são as únicas que executam nesse momento. Se recover fosse chamado no fluxo normal, não haveria panic para recuperar. Essa restrição é por design — garante que recover só seja usado no contexto correto.

Recover captura panics de outras goroutines?

Não. Cada goroutine tem sua própria pilha de chamadas, e recover só pode capturar panics da goroutine onde foi chamado. Um panic em uma goroutine filha não pode ser recuperado pela goroutine pai. Por isso, cada goroutine que pode entrar em panic precisa ter seu próprio defer/recover.

Posso usar recover múltiplas vezes na mesma função?

Apenas o primeiro recover em uma cadeia de defer captura o panic. Uma vez que recover é chamado e retorna o valor do panic, a execução retoma normalmente e os defers subsequentes não veem mais o panic. Porém, se um defer subsequente causar um novo panic, outro recover pode capturá-lo.

Qual a diferença entre recover e try/catch de outras linguagens?

Diferente de try/catch, recover em Go é intencionalmente limitado: só funciona com defer, não suporta catch por type diretamente, e não é o mecanismo primário de tratamento de erros. Go usa retorno de error como padrão principal. Recover existe apenas para situações excepcionais onde panic é inevitável, como em middleware HTTP ou workers de background.