panic, recover e defer
Esta aula aborda os mecanismos de controle de fluxo e tratamento de erros em Go: defer para adiar execuções, panic para interromper o fluxo em condições excepcionais e recover para retomar o controle. Você aprenderá a ordem de execução dos defers, como usar panic e recover corretamente e quando evitá-los, com exemplos práticos e boas práticas.
Em Go, a robustez de um programa depende muito de como lidamos com situações inesperadas. Enquanto a maioria dos erros é tratada explicitamente com retorno de valores, existem mecanismos adicionais que permitem controlar o fluxo de execução em cenários excepcionais: defer, panic e recover. Nesta aula, vamos explorar cada um deles em profundidade, entender suas regras e ver exemplos práticos de uso.
O defer é uma declaração que adia a execução de uma função até o final da função que a contém. Já o panic interrompe o fluxo normal do programa e começa a desempilhar as chamadas de função, executando os defers. O recover permite recuperar o controle após um panic, evitando que o programa termine abruptamente. Compreender esses mecanismos é essencial para escrever código limpo e resiliente.
defer e sua ordem
O defer é uma instrução que programa a execução de uma função (ou chamada de método) para o momento em que a função atual retorna. Isso é útil para garantir que recursos sejam liberados, como fechar arquivos, conexões ou mutexes, independentemente de como a função termina (normalmente ou via panic).
Uma característica crucial é que os defers são executados em ordem LIFO (Last In, First Out), ou seja, o último defer declarado é o primeiro a ser executado. Isso pode surpreender, mas é uma regra consistente que deve ser lembrada.
package main
import "fmt"
func main() {
defer fmt.Println("primeiro defer")
defer fmt.Println("segundo defer")
defer fmt.Println("terceiro defer")
fmt.Println("corpo da função")
}
// Saída:
// corpo da função
// terceiro defer
// segundo defer
// primeiro deferNo exemplo acima, a saída mostra que os defers são executados na ordem inversa da declaração. Isso é importante quando você tem dependências: por exemplo, se você abre um arquivo e depois um banco de dados, você quer fechar o banco antes do arquivo, então o defer correspondente ao banco deve ser declarado depois.
Outro ponto: os argumentos de uma função adiada são avaliados no momento em que o defer é executado, não quando a função é chamada? Na verdade, os argumentos são avaliados imediatamente, no momento em que o defer é declarado. Isso pode causar confusão, mas é uma regra importante.
func exemplo() {
x := 10
defer fmt.Println(x) // imprime 10, não o valor final de x
x = 20
}
Nesse código, fmt.Println(x) recebe o valor 10 no momento do defer, mesmo que x seja alterado depois. Para capturar o valor final, você pode usar uma closure: defer func() { fmt.Println(x) }().
panic
O panic é uma função embutida que interrompe o fluxo normal de execução e inicia o processo de desempilhar a pilha de chamadas. Quando um panic acontece, todas as funções adiadas (defer) na pilha são executadas, e então o programa termina com uma mensagem de erro e uma stack trace (a menos que seja recuperado).
O panic é usado para indicar condições que não deveriam ocorrer, como erros de programação ou estados inválidos que tornam impossível continuar. Por exemplo, acessar um índice fora dos limites de uma slice causa um panic automático.
func dividir(a, b int) int {
if b == 0 {
panic("divisão por zero")
}
return a / b
}
func main() {
dividir(10, 0)
fmt.Println("Isso não será impresso")
}
Quando panic é chamado, a execução é interrompida imediatamente e a função main termina com uma mensagem de erro. É importante notar que panic não é para tratamento de erros comum; deve ser usado apenas em situações excepcionais e irrecuperáveis.
recover
O recover é uma função embutida que permite retomar o controle após um panic. Ela só tem efeito quando chamada dentro de uma função adiada (defer). Se chamada em qualquer outro momento, retorna nil.
Quando um panic ocorre, a pilha é desempilhada, executando os defers. Se em algum desses defers houver uma chamada a recover, o panic é "capturado" e a execução continua a partir do ponto após o defer que chamou recover.
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recuperado do panic:", r)
}
}()
fmt.Println("Antes do panic")
panic("algo deu errado")
fmt.Println("Depois do panic")
}
// Saída:
// Antes do panic
// Recuperado do panic: algo deu errado
No exemplo, o recover dentro do defer captura o valor passado ao panic e permite que o programa continue, mas apenas até o final da função main (não há mais código depois). Em um cenário real, você pode usar recover para evitar que um servidor web caia por causa de um panic em uma goroutine, por exemplo.
É importante notar que recover só funciona se for chamado diretamente na função adiada, não em uma função chamada por ela. Ou seja, deve estar no mesmo defer que está sendo executado durante o desempilhamento.
Quando (não) usar
O uso de panic e recover deve ser raro e bem justificado. Aqui estão algumas diretrizes:
Quando usar panic:
- Erros de programação, como índices fora dos limites, que não deveriam ocorrer se o código estiver correto.
- Condições que tornam o programa incapaz de continuar de forma segura, como falha ao inicializar um componente crítico.
- Em bibliotecas, pode-se usar panic para sinalizar uso incorreto da API, mas é preferível retornar erros.
Quando não usar panic:
- Para tratamento de erros comuns, como falha de I/O ou validação de entrada. Nesses casos, use retornos de erro explícitos.
- Em código de produção, evite panic em caminhos normais de execução, pois pode derrubar o programa inteiro.
- Não use panic como mecanismo de controle de fluxo, como para sair de loops ou funções aninhadas.
Quando usar recover:
- Em servidores ou aplicações de longa duração, para evitar que um panic em uma goroutine derrube todo o processo.
- Em bibliotecas que precisam capturar panics internos e convertê-los em erros.
- Em testes, para verificar que um panic ocorre quando esperado.
Quando não usar recover:
- Para ignorar panics silenciosamente, pois isso pode esconder bugs.
- Em funções que não têm um propósito claro para recuperar, como uma função simples que não lida com recursos externos.
Uma boa prática é usar defer para garantir limpeza de recursos e, em conjunto, um recover para logar o erro e continuar ou encerrar graciosamente.
Boas práticas e observações finais
A combinação de defer, panic e recover é poderosa, mas deve ser usada com moderação. Aqui vão algumas dicas:
- Sempre use
deferpara fechar recursos, mesmo que você espere que a função retorne com erro. - Quando usar
recover, registre o erro (log) para diagnóstico, não apenas engula. - Tenha cuidado com o uso de
deferem loops: muitos defers podem acumular memória até o final da função, então prefira chamar a função diretamente ou usar um bloco separado. - Evite
panicem bibliotecas públicas; retorne erros sempre que possível. - Em testes, você pode usar
recoverpara verificar que uma função entra em pânico, mas prefira usar a funçãoassert.Panicsde bibliotecas como testify.
Com essas ferramentas, você pode escrever código Go mais robusto e resiliente, lidando com situações inesperadas de forma controlada.
Exercícios
- Exercício 1: Escreva uma função que use
deferpara imprimir "Fim da função" e que receba um inteiro. Dentro da função, imprima o valor do inteiro e depois retorne. Teste com diferentes valores. - Exercício 2: Crie um programa que demonstre a ordem de execução dos
defers: declare trêsdefers com mensagens diferentes e execute o programa. Explique a saída. - Exercício 3: Escreva uma função que receba dois inteiros e retorne a divisão. Se o divisor for zero, a função deve entrar em pânico com uma mensagem "divisão por zero". Chame a função em
maine verifique o comportamento. - Exercício 4: Implemente uma função que use
recoverpara capturar um panic e imprimir a mensagem. Teste chamando uma função que entre em pânico. - Exercício 5: Crie um programa que simule um servidor simples: uma função que processa requisições e pode entrar em pânico. Use
recoverna função principal para capturar panics de goroutines e continuar processando as próximas requisições.
package main
import "fmt"
func exemplo(n int) {
defer fmt.Println("Fim da função")
fmt.Println("Valor:", n)
}
func main() {
exemplo(42)
}
package main
import "fmt"
func main() {
defer fmt.Println("primeiro")
defer fmt.Println("segundo")
defer fmt.Println("terceiro")
}
// Saída: terceiro, segundo, primeiro (ordem LIFO)
package main
import "fmt"
func dividir(a, b int) int {
if b == 0 {
panic("divisão por zero")
}
return a / b
}
func main() {
fmt.Println(dividir(10, 2))
fmt.Println(dividir(10, 0)) // causa panic
}
package main
import "fmt"
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recuperado:", r)
}
}()
panic("ops")
}
package main
import (
"fmt"
"time"
)
func processar(id int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Erro na requisição %d: %v\n", id, r)
}
}()
if id == 3 {
panic("falha inesperada")
}
fmt.Printf("Processando requisição %d\n", id)
}
func main() {
for i := 1; i <= 5; i++ {
go processar(i)
}
time.Sleep(time.Second)
}