DEV Community

Cover image for Adiar, entrar em pânico e recuperar
Douglas Held Pacito
Douglas Held Pacito

Posted on

Adiar, entrar em pânico e recuperar

Estudando Go, me deparei com o uso de 3 companheiros que parecem andar sempre juntos, ou quase...

Estou falando do defer, panic e o recover. Go tem uma maneira bem peculiar (e poderosa) de lidar com exceções, diferente do tradicional try/catch/finally

defer

Agenda uma função para ser executada no fim da função atual.

Se temos uma instrução defer no escopo de uma função, ela será adiada até que a função termine, seja normalmente, com return ou mesmo em caso de panic.

Para ficar mais claro, veremos alguns exemplos

1. Ao chegar no final da função normalmente

O defer é chamado no final da função

func main() {
    defer fmt.Println("defer executado")
    fmt.Println("fim da função")
}
// Saída:
// fim da função
// defer executado
Enter fullscreen mode Exit fullscreen mode

2. Quando há um return explícito

Mesmo que você use return no meio da função, o defer que foi adiado (deferred) ainda será executado antes de sair.

Caso exista uma instrução defer após o return essa instrução não será adiada

func main() {
    defer fmt.Println("defer antes do return")
    if true {
        fmt.Println("faz algo e retorna")
        return
    }
    defer fmt.Println("defer após o return") // não será adiada
}
// Saída:
// faz algo e retorna
// defer antes do return
Enter fullscreen mode Exit fullscreen mode

3. Quando ocorre um panic

Se a função entrar em pânico, o defer ainda é chamado

func main() {
    defer fmt.Println("defer mesmo com panic")
    panic("ops!")
}
// Saída:
// defer mesmo com panic
// panic: ops!
Enter fullscreen mode Exit fullscreen mode

4. Quando há vários defer

Podem existir vários defer dentro de uma função mas temos que lembrar que a execução será na ordem inversa à que foram definidas ou seja em pilha (LIFO - Last In First Out).

func main() {
    defer fmt.Println("último")
    defer fmt.Println("segundo")
    defer fmt.Println("primeiro")
}
// Saída:
// primeiro
// segundo
// último
Enter fullscreen mode Exit fullscreen mode

5. Quando recover é chamado dentro de um defer

Se o defer contiver um recover, ele pode capturar um panic e impedir o crash.

func main() {
    someFunction()
    fmt.Println("vida que segue...")
}

func someFunction() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recuperado:", r)
        }
    }()
    panic("algo deu errado")
}
// Saída
// recuperado: algo deu errado
// vida que segue...
Enter fullscreen mode Exit fullscreen mode

os.Exit() interrompe a execução dos defer

Se você usar os.Exit() os defers não serão executados, pois o programa termina imediatamente.

func main() {
    defer fmt.Println("isso não será exibido")
    os.Exit(1)
}
// Saída
// exit status 1
Enter fullscreen mode Exit fullscreen mode

A instrução defer é ideal para cleanup de recursos sendo usada com frequência em operações aos pares, como:

  • abrir e fechar;
  • conectar e desconectar;
  • travar e destravar.

O lugar correto para uma instrução defer que libera recurso é logo após este ter sido obtido com sucesso.

func main() {
    someFunction("https://www.google.com")
}

func someFunction(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close() // evita vazamento de recurso

    // restante do código que omiti

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Ufa, chegamos no fim da instrução defer espero que com esses exemplos consegui ajudar a clarear seu uso.

Agora, bora entrar em pânico? 😅

panic

Interrompe o fluxo normal do programa. Go possui um sistema de tipos que captura muitos erros em tempo de compilação mas existem outros erros que são capturados em runtime (tempo de execução), esses erros podem causar pânico.

Existem vários exemplos de erro sendo executado com panic em runtime, mostrarei 3

1. Acesso fora dos limites de um array

Go não permite acessar índices inválidos. Isso causa um panic imediato.

func main() {
    s := []int{1, 2, 3}
    fmt.Println(s[5])
}
// Saída
// panic: runtime error: index out of range [5] with length 3
Enter fullscreen mode Exit fullscreen mode

2. Desreferenciar ponteiro nil

A tentativa de acessar o valor de um ponteiro que aponta para nil resulta em panic.

func main() {
    var p *int
    fmt.Println(*p)
}
// Saída
// panic: runtime error: invalid memory address or nil pointer dereference
Enter fullscreen mode Exit fullscreen mode

3. Divisão por zero

Go trata divisão por zero como um erro em tempo de execução com panic.

func main() {
    x := 0
    fmt.Println(10 / x)
}
// Saída
// panic: runtime error: integer divide by zero
Enter fullscreen mode Exit fullscreen mode

Podemos chamar a função panic

Nem todo o pânico é gerado pelo runtime, podemos chamar a função panic diretamente, ela aceita qualquer valor como argumento.

Mas quando devemos gerar um pânico?

O melhor momento de gerar um pânico é em situações "impossíveis" que do ponto de vista lógico não poderia acontecer, vamos simular algo randômico tendo os seguintes valores:

"Spades", "Hearts", "Diamonds", "Clubs"

Mas para fim didático colocaremos mais um valor "Joker" que pela lógica não deveria existir.

Copie o código e tente executar algumas vezes até cair na carta Coringa (Joker)

func main() {
    suits := []string{"Spades", "Hearts", "Diamonds", "Clubs", "Joker"}

    switch s := suits[rand.Intn(len(suits))]; s {
    case "Spades":
        fmt.Println("♠ Naipe: Spades")
    case "Hearts":
        fmt.Println("♥ Naipe: Hearts")
    case "Diamonds":
        fmt.Println("♦ Naipe: Diamonds")
    case "Clubs":
        fmt.Println("♣ Naipe: Clubs")
    default:
        panic(fmt.Sprintf("🤡 Naipe inválido: %q", s))
    }
}
Enter fullscreen mode Exit fullscreen mode

Cautela

O pânico, quando é executado, faz o programa terminar, então devemos usa-lo somente em erros graves.

Erros como falhas de E/S ou erro de entrada incorreta são erros que podem ser considerados como "esperados", devemos evitar usar panic se esse for o caso use os valores de error, tornando o tratamento desses erros mais elegante. Deixe o panic para ser usado em erros inesperados ou fatais que tornam impossível continuar com o programa.

recover

Permite interceptar um panic e evitar que a aplicação morra.

Você pode estar se perguntado: Então, quando fazer um recover?

Vamos ver um exemplo simples onde a mensagem "FIM" só será exibida se:

  • não houver nenhum panic;
  • ou se houver um panic ter um tratamento usando recover
func main() {
    fmt.Println("Início")
    safe()
    fmt.Println("Fim") // Nesse caso, só será impresso se recover for chamado
}

func safe() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("⚠️ Recuperado do pânico:", r)
        }
    }()

    fmt.Println("Executando algo perigoso...")
    panic("💥 Algo deu errado!") // Simulando erro grave
}
// Saída
// Início
// Executando algo perigoso...
// ⚠️ Recuperado do pânico: 💥 Algo deu errado!
// Fim
Enter fullscreen mode Exit fullscreen mode

O que aconteceu?

  1. safe() entra em ação
  2. Um panic é disparado
  3. O defer detecta o pânico com recover
  4. A aplicação segue viva!

Esse padrão é útil em middlewares, servidores e até quando lidamos com recursos externos como arquivos ou conexões.

Agora que já entendeu, vejamos algo mais complexo.

defer + panic + recover

Pense no seguinte cenário:

  • o usuário faz uma requisição para uma alteração no banco de dados
  • essa alteração é custosa e precisamos abrir uma transação
  • antes de encerar a transação acontece um erro crítico, um panic

Isso pode levar a:

  • Deadlocks
  • Conexões penduradas
  • Consumo excessivo de recursos
  • Inconsistência de dados em alguns casos

Para esse caso podemos usar o defer + recover para fazer um rollback:

package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/lib/pq"
)

func main() {
    connStr := "postgres://postgres:postgres@localhost:5432/db_name?sslmode=disable"
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    err = transferirComSeguranca(db, 1, 2, 100)
    if err != nil {
        log.Println("Erro:", err)
    } else {
        log.Println("Transferência concluída com sucesso!")
    }
}

func transferirComSeguranca(db *sql.DB, deID, paraID, valor int) (err error) {
    tx, err := db.Begin()
    if err != nil {
        return err
    }

    // Defer para rollback seguro
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
            err = fmt.Errorf("panic recuperado: %v", r)
        } else if err != nil {
            tx.Rollback()
        } else {
            err = tx.Commit()
        }
    }()

    // Debita da conta de origem
    _, err = tx.Exec("UPDATE contas SET saldo = saldo - $1 WHERE id = $2", valor, deID)
    if err != nil {
        return err
    }

    // Simula erro crítico (como divisão por zero, ou nil pointer, ou qualquer erro que gere um panic)
    panic("falha catastrófica durante a transferência")

    // Credita na conta de destino
    _, err = tx.Exec("UPDATE contas SET saldo = saldo + $1 WHERE id = $2", valor, paraID)
    if err != nil {
        return err
    }

    return nil
}
// Saída
// 2025/05/23 14:40:07 Erro: panic recuperado: falha catastrófica durante a transferência
Enter fullscreen mode Exit fullscreen mode

Legal né?
Mas vamos olhar melhor para essa parte do código:

defer func() {
    if r := recover(); r != nil { // 1 - se houver panic
        tx.Rollback()
        err = fmt.Errorf("panic recuperado: %v", r)
    } else if err != nil { // 2 - se houver erro comum
        tx.Rollback()
    } else { // 3 - se tudo der certo
        err = tx.Commit()
    }
}()
Enter fullscreen mode Exit fullscreen mode

1 - panic -> recover() + Rollback

Se a função entrou em pânico, o recover() captura o erro e:

  • faz rollback para evitar transação presa
  • transforma o pânico em erro amigável com fmt.Errorf(...)

2 - err != nil -> erro esperado (ex: SQL inválido)

Se não houve panic, mas alguma linha antes retornou erro (ex: tx.Exec(...)), então:

  • a transação ainda não foi finalizada
  • precisamos fazer rollback() também

3 - else -> deu tudo certo

Só nesse caso fazemos commit(), salvamos tudo no banco

Dica

Use panic com moderação. Em geral, prefira retornar erros comuns (error). Mas saber lidar com um panic pode evitar muitos sustos.

Top comments (0)