Go Admin

defer, panic e recover em Go: Controle de Fluxo Excepcional na Prática Já leu

Entendendo o Fluxo Excepcional em Go Go é uma linguagem que não segue o modelo tradicional de exceções com try-catch que você pode conhecer de linguagens como Java ou Python. Em vez disso, Go utiliza um mecanismo bem diferente para lidar com erros e situações excepcionais: , e . Esses três elementos trabalham juntos para fornecer controle fino sobre o fluxo de execução, especialmente em cenários onde você precisa garantir limpeza de recursos ou recuperação de falhas críticas. Diferentemente de um sistema de exceções tradicional, Go força o programador a ser explícito sobre o que pode dar errado e como tratar cada situação. Isso torna o código mais previsível e seguro. Porém, e são ferramentas poderosas para situações verdadeiramente excepcionais — não use-as como um substituto para tratamento de erros convencional com o tipo . Defer: Garantindo Execução com Segurança O Conceito Fundamental é uma declaração que adia a execução de uma função até que a função envolvente retorne. Você

Entendendo o Fluxo Excepcional em Go

Go é uma linguagem que não segue o modelo tradicional de exceções com try-catch que você pode conhecer de linguagens como Java ou Python. Em vez disso, Go utiliza um mecanismo bem diferente para lidar com erros e situações excepcionais: defer, panic e recover. Esses três elementos trabalham juntos para fornecer controle fino sobre o fluxo de execução, especialmente em cenários onde você precisa garantir limpeza de recursos ou recuperação de falhas críticas.

Diferentemente de um sistema de exceções tradicional, Go força o programador a ser explícito sobre o que pode dar errado e como tratar cada situação. Isso torna o código mais previsível e seguro. Porém, panic e recover são ferramentas poderosas para situações verdadeiramente excepcionais — não use-as como um substituto para tratamento de erros convencional com o tipo error.

Defer: Garantindo Execução com Segurança

O Conceito Fundamental

defer é uma declaração que adia a execução de uma função até que a função envolvente retorne. Você a utiliza quando precisa garantir que algo será executado, independentemente de qual caminho o código tome — seja um retorno normal ou um pânico. Pense em defer como um mecanismo de "antes de sair, faça isto".

A ordem de execução é crucial: se você declara múltiplas instruções defer, elas são executadas em ordem LIFO (Last In, First Out), como uma pilha. A última instrução defer que você declarou é a primeira a ser executada quando a função retorna.

package main

import (
    "fmt"
    "os"
)

func exemploDefer() {
    fmt.Println("1. Início da função")

    defer fmt.Println("3. Primeiro defer (executado por último)")
    defer fmt.Println("2. Segundo defer (executado primeiro)")

    fmt.Println("1.5 Meio da função")
}

func limpezaDeArquivo() error {
    arquivo, err := os.Open("dados.txt")
    if err != nil {
        return err
    }

    // Garante que o arquivo será fechado ao sair da função
    defer arquivo.Close()

    // Operações com o arquivo
    buffer := make([]byte, 100)
    arquivo.Read(buffer)

    return nil
}

func main() {
    exemploDefer()
    // Output:
    // 1. Início da função
    // 1.5 Meio da função
    // 2. Segundo defer (executado primeiro)
    // 3. Primeiro defer (executado por último)

    limpezaDeArquivo()
}

Casos de Uso Prático

O uso mais comum de defer é garantir que recursos sejam liberados: fechar arquivos, desconectar de bancos de dados, liberar locks ou realizar rollback de transações. Uma vantagem importante é que defer funciona mesmo quando ocorre um panic — você está garantindo limpeza em qualquer circunstância.

package main

import (
    "fmt"
    "sync"
)

type Recurso struct {
    nome string
    mu   sync.Mutex
}

func (r *Recurso) Adquirir() {
    r.mu.Lock()
    fmt.Printf("Recurso %s adquirido\n", r.nome)
}

func (r *Recurso) Liberar() {
    r.mu.Unlock()
    fmt.Printf("Recurso %s liberado\n", r.nome)
}

func processarComRecurso(r *Recurso) {
    r.Adquirir()
    defer r.Liberar()

    fmt.Println("Processando...")
    // Mesmo que ocorra um return ou panic aqui,
    // r.Liberar() será executado
}

func main() {
    r := &Recurso{nome: "BD_Conexão"}
    processarComRecurso(r)
}

Panic: Sinalizando Falhas Críticas

Quando e Por Que Usar Panic

panic é um mecanismo para indicar que algo verdadeiramente excepcional aconteceu — situações em que o programa não pode continuar de forma normal. Quando você chama panic, a execução atual para imediatamente, funções defer são executadas em ordem reversa, e o programa encerra com uma mensagem de erro, a menos que o pânico seja recuperado com recover.

A pergunta que você deve fazer antes de usar panic é: "Posso lidar com isso retornando um erro?" Se sim, retorne um error. Se não — se o programa realmente não pode prosseguir de forma lógica — aí você considera panic. Por exemplo, falhas em inicialização, invariantes quebradas ou condições que nunca deveriam acontecer em código bem escrito.

package main

import (
    "fmt"
    "log"
)

func inicializarConfig(arquivo string) {
    if arquivo == "" {
        panic("Arquivo de configuração não pode estar vazio")
    }
    fmt.Printf("Inicializando com arquivo: %s\n", arquivo)
}

func processarDados(dados []int, indice int) int {
    // Isso é uma falha de lógica que nunca deveria acontecer
    // em código bem escrito. Se acontecer, é um bug.
    if indice < 0 || indice >= len(dados) {
        panic(fmt.Sprintf("Índice fora do intervalo: %d", indice))
    }
    return dados[indice]
}

func exemploPanicComDefer() {
    defer fmt.Println("Limpando recursos...")

    fmt.Println("Iniciando operação")
    panic("Erro crítico: operação impossível")
    fmt.Println("Esta linha nunca será executada")
}

func main() {
    // Exemplo 1: Panic durante inicialização
    // Descomente para ver:
    // inicializarConfig("")

    // Exemplo 2: Panic com defer
    // exemploPanicComDefer()

    // Exemplo 3: Panic que nunca deveria acontecer
    dados := []int{10, 20, 30}
    resultado := processarDados(dados, 1)
    fmt.Printf("Resultado: %d\n", resultado)
}

Entendendo o Stack Unwinding

Quando panic é chamado, Go inicia o "unwinding" da pilha de chamadas. Isso significa que cada função na pilha encerra sua execução, mas antes disso, todas as suas declarações defer são executadas. Esse processo continua até que o programa seja recuperado com recover ou até que não haja mais funções na pilha.

package main

import "fmt"

func nivelTres() {
    defer fmt.Println("Defer do nível 3")
    fmt.Println("Nível 3 antes do panic")
    panic("Pânico no nível 3")
    fmt.Println("Nível 3 depois do panic (nunca executa)")
}

func nivelDois() {
    defer fmt.Println("Defer do nível 2")
    fmt.Println("Nível 2 antes de chamar nivelTres")
    nivelTres()
    fmt.Println("Nível 2 depois de nivelTres (nunca executa)")
}

func nivelUm() {
    defer fmt.Println("Defer do nível 1")
    fmt.Println("Nível 1 antes de chamar nivelDois")
    nivelDois()
    fmt.Println("Nível 1 depois de nivelDois (nunca executa)")
}

func main() {
    defer fmt.Println("Defer do main (nunca executa sem recover)")
    fmt.Println("Iniciando programa")
    nivelUm()
    fmt.Println("Fim do programa (nunca executa)")
}

Quando você executa este código, a saída será:

Iniciando programa
Nível 1 antes de chamar nivelDois
Nível 2 antes de chamar nivelTres
Nível 3 antes do panic
Defer do nível 3
Defer do nível 2
Defer do nível 1
Pânico: Pânico no nível 3

Recover: Recuperando de Panics

O Mecanismo de Recuperação

recover é uma função built-in que permite você capturar e lidar com um pânico que está sendo propagado. Ela só funciona quando chamada dentro de uma função defer — em qualquer outro contexto, recover retorna nil. Quando um pânico é recuperado, a execução retorna normalmente para a função que contém o defer com recover, permitindo que o programa continue funcionando.

O uso apropriado de recover é em pontos de entrada críticos do seu programa — handlers HTTP, workers em goroutines, processadores de eventos — onde você quer garantir que um pânico em um cliente ou tarefa não derrube todo o servidor ou thread.

package main

import (
    "fmt"
    "log"
)

func operacaoPerigosa() {
    panic("Algo deu muito errado aqui")
}

func executarComSeguranca() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Recuperado de pânico: %v\n", r)
        }
    }()

    fmt.Println("Iniciando operação perigosa")
    operacaoPerigosa()
    fmt.Println("Depois da operação (só executa se não houver pânico)")
}

func main() {
    fmt.Println("Antes de chamar executarComSeguranca")
    executarComSeguranca()
    fmt.Println("Depois de executarComSeguranca - programa continua!")
}

Padrões Avançados: Protegendo Goroutines

Um caso de uso muito comum em Go é proteger goroutines de panics que as derrubam silenciosamente. Se uma goroutine sofre pânico sem tratamento, o programa inteiro encerra. Por isso, é uma boa prática envolver workers em um recover.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()

    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("Worker %d recuperado de pânico: %v\n", id, r)
        }
    }()

    for job := range jobs {
        if job == 13 {
            panic(fmt.Sprintf("Worker %d não gosta do número 13!", id))
        }
        fmt.Printf("Worker %d processando job %d\n", id, job)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    jobs := make(chan int, 20)
    var wg sync.WaitGroup

    // Inicia 3 workers
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, jobs, &wg)
    }

    // Envia jobs (incluindo um que causa pânico)
    for j := 1; j <= 15; j++ {
        jobs <- j
    }
    close(jobs)

    wg.Wait()
    fmt.Println("Todos os workers finalizaram, programa continua!")
}

Recuperando com Contexto

Um padrão mais sofisticado é recuperar não apenas o valor do pânico, mas também informações sobre onde e por que ele ocorreu. Você pode usar debug.Stack() para obter um stack trace.

package main

import (
    "fmt"
    "log"
    "runtime/debug"
)

func processoComplexo() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("ERRO CRÍTICO: %v\n", r)
            log.Printf("Stack trace:\n%s\n", debug.Stack())
        }
    }()

    // Simula um erro profundo na lógica
    slice := []int{1, 2, 3}
    _ = slice[10] // Isso causaria um panic em código real sem bounds checking
}

func main() {
    fmt.Println("Programa iniciado")
    processoComplexo()
    fmt.Println("Programa terminado com sucesso (recuperado do pânico)")
}

Padrões e Boas Práticas

Combinando Defer, Panic e Recover de Forma Responsável

O trio defer, panic e recover é poderoso, mas deve ser usado com propósito. A mensagem central é: use panic e recover com moderação e apenas para situações verdadeiramente excepcionais. A maioria do seu código deve lidar com erros retornando valores error convencionais.

Um padrão saudável em Go é separar as responsabilidades: código de negócio retorna error, código de infraestrutura (handlers, workers, inicialização) usa panic para falhas irrecuperáveis e recover para proteção em pontos de entrada.

package main

import (
    "errors"
    "fmt"
    "log"
)

// Camada de negócio: usa error
func validarEmail(email string) error {
    if email == "" {
        return errors.New("email não pode estar vazio")
    }
    if len(email) < 3 {
        return errors.New("email muito curto")
    }
    return nil
}

// Camada de aplicação: usa panic para invariantes
func iniciarAplicacao(config map[string]string) {
    if config == nil {
        panic("configuração não pode ser nil")
    }
    if _, ok := config["DATABASE_URL"]; !ok {
        panic("DATABASE_URL não configurado")
    }
}

// Handler HTTP: usa recover para proteção
func handleRequest(email string) {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Erro não tratado no handler: %v", r)
        }
    }()

    // Chama lógica de negócio
    if err := validarEmail(email); err != nil {
        fmt.Printf("Validação falhou: %v\n", err)
        return
    }

    fmt.Printf("Email válido: %s\n", email)
}

func main() {
    // Inicialização: pode usar panic
    config := map[string]string{
        "DATABASE_URL": "postgres://localhost",
    }
    iniciarAplicacao(config)

    // Handlers: protegidos com recover
    handleRequest("user@example.com")
    handleRequest("")

    fmt.Println("Aplicação finalizou normalmente")
}

Erros vs Panics: Quando Usar Cada Um

Retorne um error quando:
- O erro faz parte do contrato normal da função
- Você espera que o chamador possa lidar com a situação
- É um erro causado por dados de entrada inválidos

Use panic quando:
- Ocorre uma violação de invariante do programa
- É uma falha durante inicialização que impossibilita continuar
- É uma situação que nunca deveria acontecer em código correto

package main

import (
    "errors"
    "fmt"
)

// Bom: retorna error para entrada inválida
func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divisão por zero")
    }
    return a / b, nil
}

// Bom: panic para invariante quebrada
func acessarSlice(s []int, i int) int {
    if i < 0 || i >= len(s) {
        panic(fmt.Sprintf("índice %d fora do range [0, %d)", i, len(s)))
    }
    return s[i]
}

// Ruim: não use panic para validação de entrada
func processarNomeRuim(nome string) {
    if nome == "" {
        panic("nome não pode estar vazio") // Use error!
    }
}

// Melhor: retorne error para validação
func processarNomeBom(nome string) error {
    if nome == "" {
        return errors.New("nome não pode estar vazio")
    }
    return nil
}

func main() {
    // Tratamento normal de erro
    resultado, err := dividir(10, 2)
    if err != nil {
        fmt.Printf("Erro: %v\n", err)
    } else {
        fmt.Printf("Resultado: %f\n", resultado)
    }
}

Conclusão

Você aprendeu que defer é um mecanismo de garantia: qualquer coisa que você declare com defer será executada quando a função retornar, independentemente de panics ou returns múltiplos. Use defer para limpeza de recursos — é uma das features mais importantes de Go.

panic sinaliza falhas verdadeiramente críticas que impedem o programa de continuar de forma lógica. Não use como substituto para tratamento de erros convencional; reserve-o para situações como falhas de inicialização ou violações de invariantes. O stack unwinding garante que seus defer statements sejam executados mesmo durante um pânico.

recover permite capturar e se recuperar de panics, mas deve ser usado estrategicamente em pontos de entrada críticos — handlers, goroutines, workers — onde você quer garantir que uma falha em uma solicitação não derrube todo o sistema. A regra de ouro é: escreva código robusto com error returns, use panic raramente, e recover apenas quando necessário proteger código concorrente ou serviços críticos.

Referências


Artigos relacionados