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
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 oreturn
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
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!
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
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...
os.Exit()
interrompe a execução dos defer
Se você usar os.Exit()
os defer
s 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
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
}
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
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
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
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))
}
}
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 usandorecover
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
O que aconteceu?
-
safe()
entra em ação - Um
panic
é disparado - O
defer
detecta o pânico comrecover
- 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
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()
}
}()
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)