defer, panic e recover no Go: quando usar
1. Visão geral
Go não possui try/catch. Em vez disso, a linguagem oferece três ferramentas: defer para limpar recursos, panic para sinalizar erros irrecuperáveis, e recover para interromper panic em uma fronteira específica.
Este artigo explica o mecanismo de cada ferramenta, as armadilhas comuns, e o mais importante: quando usar e quando não usar. Após ler, você saberá como usar defer corretamente, quando panic é apropriado, e como usar recover para manter seu servidor em pé quando um request falha.
2. defer
defer adia uma chamada de função até que a função envolvente termine. A função pode terminar com um return normal ou por causa de um panic. Em ambos os casos, as chamadas com defer ainda são executadas.
2.1. Ordem de execução LIFO
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
defer fmt.Println("defer", i)
}
fmt.Println("done")
}
Cada vez que encontra um defer, Go coloca a chamada em uma pilha própria da função. Quando main termina, as chamadas são retiradas em ordem inversa:
done
defer 3
defer 2
defer 1
Essa ordem LIFO tem um motivo. Recursos abertos depois geralmente dependem de recursos abertos antes. Por exemplo, você abre uma connection e depois abre uma transação, então a transação deve ser fechada antes da connection.
2.2. Argumentos são avaliados imediatamente
Essa é a primeira armadilha. Os argumentos da função com defer são calculados no momento em que encontra defer, não quando a chamada realmente executa.
package main
import "fmt"
func main() {
total := 10
defer fmt.Println("deferred total:", total)
defer func() {
fmt.Println("closure total:", total)
}()
total = 20
fmt.Println("current total:", total)
}
current total: 20
closure total: 20
deferred total: 10
Na primeira chamada, o valor 10 é capturado imediatamente quando encontra defer. A closure na segunda chamada é diferente. Ela faz referência à variável total, então quando executa lê o valor mais recente que é 20.
Quando você precisa do valor final, como para medir tempo ou registrar estado, use uma closure.
2.3. Modificar o valor retornado com named result
Uma closure com defer pode ler e escrever named result da função. Essa técnica é muito útil quando Close() pode retornar um erro, por exemplo ao escrever arquivo onde os dados só são realmente salvos ao fechar.
package main
import (
"errors"
"fmt"
"os"
)
func writeReport(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return err
}
defer func() {
err = errors.Join(err, f.Close())
}()
_, err = f.Write(data)
return err
}
func main() {
err := writeReport("/tmp/report.txt", []byte("Order #42: 3 items\n"))
fmt.Println("error:", err)
}
error: <nil>
A instrução return err atribui o valor à variável err, então a closure com defer executa. A closure mescla o erro de f.Close() em err usando errors.Join, que existe desde Go 1.20. errors.Join retorna nil se ambos os erros forem nil.
Se você apenas escrever defer f.Close(), um erro ao fechar o arquivo será perdido sem ninguém saber.
Nota: Para arquivo somente leitura,
defer f.Close()é suficiente. Um erro ao fechar um arquivo read-only raramente afeta os dados.
2.4. Armadilha: defer dentro de loops
func processAll(paths []string) error {
for _, p := range paths {
f, err := os.Open(p)
if err != nil {
return err
}
defer f.Close() // ERRADO: só fecha quando processAll termina
// ... ler f ...
}
return nil
}
defer está associado à função, não ao bloco ou loop. Com 10.000 arquivos, o código acima mantém 10.000 file descriptors abertos simultaneamente e facilmente encontra o erro too many open files.
O jeito de consertar é extrair o corpo do loop em uma função separada:
func processAll(paths []string) error {
for _, p := range paths {
if err := processFile(p); err != nil {
return err
}
}
return nil
}
func processFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // fecha assim que processFile retorna
// ... ler f ...
return nil
}
3. panic
panic interrompe a execução normal da goroutine atual. Go começa a desenrolar a pilha: cada função retorna para cima, e os defer de cada função ainda são executados. Quando chega ao topo da pilha da goroutine e não há ninguém chamando recover, o programa imprime o stack trace e sai com código 2.
3.1. Observar o processo de unwinding
package main
import "fmt"
func main() {
defer fmt.Println("main: deferred")
processOrder()
fmt.Println("never printed")
}
func processOrder() {
defer fmt.Println("processOrder: deferred")
panic("inventory service returned invalid state")
}
processOrder: deferred
main: deferred
panic: inventory service returned invalid state
goroutine 1 [running]:
main.processOrder()
/tmp/main.go:13 +0x...
exit status 2
Você vê que ambos os defer são executados antes do programa morrer. Essa é exatamente a razão pela qual defer mu.Unlock() ou defer tx.Rollback() ainda são seguros quando há panic.
Panic também pode vir do runtime, não apenas de chamadas panic() explícitas. Casos comuns incluem acessar índice fora do intervalo, desreferenciar um ponteiro nil, escrever em um map nil, ou dividir um inteiro por 0.
3.2. Quando fazer panic
A regra geral do Go: erros previsíveis retornam error. Por exemplo, arquivo não existe, entrada com formato inválido, ou timeout de rede. Use panic apenas em alguns casos específicos.
1. Erro de programação, isto é, uma invariante foi violada. É um estado “impossível de acontecer” se o código estiver correto, e continuar executando só vai corromper dados.
func (s OrderStatus) String() string {
switch s {
case Pending:
return "PENDING"
case Paid:
return "PAID"
case Shipped:
return "SHIPPED"
}
panic(fmt.Sprintf("unknown OrderStatus: %d", s))
}
2. Funções MustXxx usadas durante inicialização. A biblioteca padrão tem regexp.MustCompile e template.Must. Você pode escrever funções similares para configuração:
package config
import (
"fmt"
"strconv"
)
func MustParsePort(s string) int {
port, err := strconv.Atoi(s)
if err != nil || port < 1 || port > 65535 {
panic(fmt.Sprintf("invalid port %q", s))
}
return port
}
var emailPattern = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
Se a configuração estiver errada, a aplicação não deveria iniciar. Fazer panic no momento do start-up ajuda a detectar erros cedo, em vez de deixar o servidor rodando com estado corrompido.
Nota: Não use
MustXxxcom dados de usuário ou requests. Uma entrada ruim não deve ser capaz de derrubar todo o processo.
4. recover
recover() interrompe o processo de unwinding e retorna o valor passado para panic. Essa função só funciona quando chamada diretamente dentro de uma deferred function. Chamada em qualquer outro lugar, retorna nil e não faz nada.
4.1. Converter panic em error
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("safeDivide(%d, %d): %v", a, b, r)
}
}()
return a / b, nil
}
func main() {
fmt.Println(safeDivide(10, 2))
fmt.Println(safeDivide(10, 0))
}
5 <nil>
0 safeDivide(10, 0): runtime error: integer divide by zero
Quando b == 0, o runtime faz panic. A closure com defer captura o panic usando recover() e então atribui ao named result err. A função retorna normalmente com result mantendo o valor zero que é 0.
Este exemplo é apenas para ilustração. Na prática, você deveria verificar b == 0 e retornar error diretamente.
4.2. Armadilha: recover deve ser chamado diretamente
func logPanic() {
if r := recover(); r != nil {
log.Println("recovered:", r)
}
}
func handlerA() {
defer logPanic() // OK: logPanic é a deferred function
panic("boom A")
}
func handlerB() {
defer func() {
logPanic() // NÃO funciona: recover está um nível mais fundo
}()
panic("boom B")
}
handlerA recupera com sucesso. handlerB ainda faz o programa cair, porque recover é chamado dentro de logPanic, e logPanic por sua vez é chamada por uma deferred function, mas ela mesma não é uma deferred function.
4.3. Armadilha: recover não captura panic de outra goroutine
Cada goroutine tem sua própria pilha. Um panic em uma goroutine filha derruba todo o processo, não importa se main tem recover ou não.
func main() {
defer func() { recover() }() // inútil para a goroutine abaixo
go func() {
panic("worker crashed") // processo ainda morre
}()
time.Sleep(time.Second)
}
Se você rodar uma job em background mas não consegue controlar todo o código dentro, envolva-a:
func SafeGo(name string, fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine %s panicked: %v\n%s", name, r, debug.Stack())
}
}()
fn()
}()
}
debug.Stack() vem do package runtime/debug. Isso ajuda a registrar stack trace suficiente para você debugar depois.
4.4. Recover na fronteira HTTP
Um lugar apropriado para colocar recover é a fronteira do request. Um request com erro não deveria levar outros requests junto.
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
if rec == http.ErrAbortHandler {
panic(rec) // deixe net/http lidar com abort como projetado
}
log.Printf("panic on %s %s: %v\n%s", r.Method, r.URL.Path, rec, debug.Stack())
http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
net/http já faz recover de panic para cada connection, mas ela apenas registra e depois fecha a connection, então o cliente não recebe uma response apropriada. O middleware acima retorna um código 500 claro, e também registra method e path para fácil rastreamento.
5. Escrever testes para panic e recover
Go usa o package testing padrão, sem dependências externas.
package config
import (
"strings"
"testing"
)
func TestSafeDivide_ByZero_ReturnsError(t *testing.T) {
result, err := safeDivide(10, 0)
if err == nil {
t.Fatal("expected error, got nil")
}
if result != 0 {
t.Errorf("expected result 0, got %d", result)
}
if !strings.Contains(err.Error(), "divide by zero") {
t.Errorf("unexpected error message: %v", err)
}
}
func TestMustParsePort_InvalidInput_Panics(t *testing.T) {
defer func() {
r := recover()
if r == nil {
t.Fatal("expected panic, got none")
}
if msg, _ := r.(string); !strings.Contains(msg, `"abc"`) {
t.Errorf("unexpected panic value: %v", r)
}
}()
MustParsePort("abc")
}
func TestMustParsePort_ValidInput(t *testing.T) {
if got := MustParsePort("8080"); got != 8080 {
t.Errorf("expected 8080, got %d", got)
}
}
go test ./... -v
=== RUN TestSafeDivide_ByZero_ReturnsError
--- PASS: TestSafeDivide_ByZero_ReturnsError (0.00s)
=== RUN TestMustParsePort_InvalidInput_Panics
--- PASS: TestMustParsePort_InvalidInput_Panics (0.00s)
=== RUN TestMustParsePort_ValidInput
--- PASS: TestMustParsePort_ValidInput (0.00s)
PASS
O segundo teste usa defer e recover para afirmar que a função faz panic, e panic com a mensagem correta. Se você usar testify, assert.PanicsWithValue dá um resultado similar com menos código.
6. Quando NÃO usar
- Não use panic/recover como exception. Escrever
panic(ErrNotFound)e depois recover em uma camada acima torna o fluxo de controle difícil de ler, e vai contra a convenção do Go. Retorneerror. - Não faça panic em biblioteca pública quando o erro vem da entrada do chamador. Usuários da sua biblioteca não esperam que o processo deles morra.
- Não faça recover e depois ignore silenciosamente. Pelo menos registre o valor do panic e o stack trace. Se o estado ficou corrompido, como dados compartilhados escritos pela metade, deixe o processo morrer e deixe o orchestrator reiniciar.
- Não use
deferem loops longos (veja seção 2.4). - Não se preocupe muito com performance de
defer. Desde Go 1.14, open-coded defer custa quase nada na maioria dos casos. Só otimize se o profiler indicar que é um ponto crítico.
Nota: A partir de Go 1.21,
panic(nil)não fazrecover()retornarnilmais. Em vez disso,recover()retorna*runtime.PanicNilError. Código antigo usandorecover() == nilpara verificar “não há panic” é agora mais preciso.
7. Conclusão
Essas três ferramentas têm papéis muito diferentes:
deferé uma ferramenta do dia a dia para limpar recursos. Lembre-se de dois pontos: argumentos são avaliados imediatamente quando encontradefer, edeferestá associado à função, não ao loop. Combinado com named result,deferajuda você a não perder erros deClose().panicé para erros de programação e erros de inicialização irrecuperáveis. Para outros erros, retorneerror.recoversó deveria aparecer na fronteira: middleware HTTP, goroutine worker, ou chamada para código que você não controla. Deve ser chamado diretamente dentro de uma deferred function, e só captura panic dessa própria goroutine.
Dominando essas fronteiras, você escreverá código Go tanto seguro quanto fiel ao espírito da linguagem: erro é um valor, panic é uma exceção genuína.