Padrões com defer
Nesta aula, exploramos o uso da palavra-chave defer em Go para gerenciar recursos de forma elegante e segura, cobrindo desde liberação de recursos e fechamento de arquivos até armadilhas comuns como loops e avaliação de argumentos. Inclui exemplos práticos e exercícios para fixação.
O defer é uma das palavras-chave mais úteis em Go para garantir que ações de limpeza sejam executadas, independentemente de como a função termina (retorno normal, panic ou erro). Ele agendia uma chamada de função para ser executada imediatamente antes do retorno da função envolvente. Isso é especialmente útil para liberar recursos como arquivos abertos, conexões de rede, locks de mutex, etc.
Nesta aula, vamos explorar padrões comuns com defer, entender como ele funciona internamente e evitar armadilhas que podem levar a bugs sutis. O defer é uma ferramenta poderosa, mas seu uso inadequado pode causar problemas de desempenho ou lógica.
Liberação de recursos
O uso mais comum de defer é garantir que recursos como memória alocada, locks ou conexões sejam liberados. Em Go, não há destrutores automáticos, então o defer é a maneira idiomática de fazer cleanup.
Por exemplo, ao usar um mutex para proteger uma seção crítica, podemos usar defer para garantir que o unlock seja chamado, mesmo que ocorra um panic ou retorno antecipado:
package main
import (
"fmt"
"sync"
)
var (
mu sync.Mutex
counter int
)
func increment() {
mu.Lock()
defer mu.Unlock()
counter++
fmt.Println("Counter:", counter)
}
func main() {
for i := 0; i < 5; i++ {
go increment()
}
// Aguarda as goroutines terminarem (exemplo simples)
fmt.Scanln()
}
Outro exemplo é a liberação de memória alocada com new ou make, embora o garbage collector cuide disso, o defer pode ser usado para ações específicas como fechar um canal ou liberar um pool de buffers.
Fechamento de arquivos
O padrão mais clássico é o fechamento de arquivos. Ao abrir um arquivo, devemos garantir que ele seja fechado após o uso. Com defer, isso fica simples e seguro:
package main
import (
"bufio"
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Executado quando a função retorna
scanner := bufio.NewScanner(file)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
return scanner.Err()
}
func main() {
err := readFile("example.txt")
if err != nil {
fmt.Println("Erro:", err)
}
}
Note que o defer é colocado logo após a verificação de erro, garantindo que o arquivo seja fechado mesmo se ocorrer um erro durante a leitura. A ordem de execução dos defers é LIFO (last-in, first-out), útil quando múltiplos recursos são abertos.
Armadilhas (loop, argumentos avaliados)
Embora defer seja simples, existem armadilhas comuns:
1. Defer em loops: Se você colocar defer dentro de um loop, as chamadas deferidas só serão executadas no final da função, não a cada iteração. Isso pode acumular recursos e causar vazamentos. Exemplo problemático:
// ERRADO: defers acumulam até o final da função
for i := 0; i < 10; i++ {
f, _ := os.Open(fmt.Sprintf("file%d.txt", i))
defer f.Close()
// processa o arquivo...
} // os arquivos só serão fechados no final da função!
Solução: mover o defer para uma função auxiliar ou usar um bloco anônimo:
for i := 0; i < 10; i++ {
func() {
f, err := os.Open(fmt.Sprintf("file%d.txt", i))
if err != nil {
log.Fatal(err)
}
defer f.Close()
// processa...
}() // a função anônima é executada imediatamente, e o defer roda ao final dela
}
2. Argumentos avaliados imediatamente: Os argumentos de uma função deferida são avaliados no momento em que o defer é registrado, não quando a função deferida é executada. Isso pode causar surpresas:
func main() {
x := 10
defer fmt.Println("x =", x) // imprime 10, não 20
x = 20
}
Para capturar o valor final, use um closure ou ponteiro:
func main() {
x := 10
defer func() {
fmt.Println("x =", x) // imprime 20
}()
x = 20
}
3. Defer e recover: O defer é usado com recover para capturar panics, mas lembre-se de que recover só funciona dentro de um defer.
Exemplos
Vamos ver exemplos mais elaborados que combinam defer com outras funcionalidades:
Exemplo 1: Medição de tempo de execução
package main
import (
"fmt"
"time"
)
func trackTime(start time.Time, name string) {
elapsed := time.Since(start)
fmt.Printf("%s levou %s\n", name, elapsed)
}
func slowFunction() {
defer trackTime(time.Now(), "slowFunction")
time.Sleep(2 * time.Second)
}
func main() {
slowFunction()
}
Exemplo 2: Múltiplos defers com ordem LIFO
package main
import "fmt"
func main() {
defer fmt.Println("primeiro")
defer fmt.Println("segundo")
defer fmt.Println("terceiro")
fmt.Println("corpo")
}
// Saída:
// corpo
// terceiro
// segundo
// primeiro
Exemplo 3: Defer com recover para tratamento de panic
package main
import "fmt"
func safeDivision(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic: %v", r)
}
}()
result = a / b
return
}
func main() {
res, err := safeDivision(10, 0)
if err != nil {
fmt.Println("Erro:", err)
} else {
fmt.Println("Resultado:", res)
}
}
Boas práticas
- Sempre coloque o defer imediatamente após a aquisição do recurso, para não esquecer.
- Evite defer dentro de loops; use funções anônimas para escopo.
- Lembre-se que os argumentos são avaliados no momento do defer; use closures se precisar do valor final.
- Use defer para cleanup, não para lógica principal; isso melhora a legibilidade.
Referências
- Effective Go: defer
- Defer, Panic, and Recover - The Go Blog
- Go builtin: defer
- Go by Example: Defer
- Understanding defer in Go - DigitalOcean
Exercícios
Escreva uma função que escreva uma linha em um arquivo e garanta que o arquivo seja fechado ao final, mesmo que ocorra um erro.
✓ Resposta:func writeFile(filename, text string) error { f, err := os.Create(filename) if err != nil { return err } defer f.Close() _, err = f.WriteString(text) if err != nil { return err } return nil }O que o seguinte código imprime? Explique.
func main() { for i := 0; i < 3; i++ { defer fmt.Println(i) } }✓ Resposta: Imprime 2, 1, 0 (ordem LIFO). Cada defer registra o valor de i no momento do registro (0,1,2), e ao final da função são executados em ordem reversa.Corrija o código abaixo para que o arquivo seja fechado a cada iteração, não apenas no final:
for i := 0; i < 10; i++ { f, _ := os.Open(fmt.Sprintf("file%d.txt", i)) defer f.Close() // processa }✓ Resposta:for i := 0; i < 10; i++ { func() { f, err := os.Open(fmt.Sprintf("file%d.txt", i)) if err != nil { log.Fatal(err) } defer f.Close() // processa }() }Escreva uma função que retorne uma mensagem de erro se um panic ocorrer, usando defer e recover.
✓ Resposta:func safeCall(fn func()) (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic: %v", r) } }() fn() return }Qual a saída do código abaixo? Por que?
func main() { x := 1 defer fmt.Println(x) defer func() { fmt.Println(x) }() x = 2 }✓ Resposta: Imprime 2 (do closure) e depois 1 (do defer direto). O closure captura a variável x por referência, então vê o valor final 2. O defer direto avalia o argumento no momento do registro, que era 1.