Padrões com defer
Esta aula explora padrões de uso da instrução defer em Go, incluindo liberação de recursos, fechamento de arquivos e armadilhas comuns como loops e avaliação de argumentos.
A instrução defer em Go é uma ferramenta poderosa para garantir que uma chamada de função seja executada antes do retorno da função atual, independentemente de como ela termina (normalmente ou com pânico). Ela é frequentemente usada para limpeza de recursos, como fechar arquivos, desbloquear mutexes ou liberar conexões de rede.
Entender os padrões e as armadilhas do defer é essencial para escrever código Go idiomático e seguro. Nesta aula, vamos explorar os usos mais comuns, os cuidados necessários e exemplos práticos que solidificam o aprendizado.
Liberação de recursos
Um dos usos mais comuns de defer é garantir que recursos como mutexes, conexões de banco de dados ou buffers sejam liberados adequadamente. Ao usar defer imediatamente após adquirir o recurso, você reduz a chance de esquecer de liberá-lo em todos os caminhos de execução.
Por exemplo, ao trabalhar com um mutex, é uma prática comum travar o mutex e imediatamente adiar seu destravamento:
var mu sync.Mutex
func atualizarDado() {
mu.Lock()
defer mu.Unlock()
// ... operações críticas ...
}Isso garante que, mesmo que a função entre em pânico ou retorne mais cedo, o mutex será liberado. O mesmo padrão se aplica a outros recursos, como conexões de rede ou transações de banco de dados.
Fechamento de arquivos
Outro padrão clássico é o fechamento de arquivos. Depois de abrir um arquivo, é crucial fechá-lo para evitar vazamento de descritores de arquivo. Usar defer torna o código mais limpo e seguro:
func lerArquivo(nome string) ([]byte, error) {
f, err := os.Open(nome)
if err != nil {
return nil, err
}
defer f.Close()
dados, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return dados, nil
}Neste exemplo, defer f.Close() é executado quando a função retorna, garantindo que o arquivo seja fechado mesmo se ocorrer um erro na leitura. É importante notar que, se você estiver escrevendo em um arquivo, pode ser necessário verificar o erro de fechamento explicitamente (por exemplo, em buffers com flush).
Armadilhas (loop, argumentos avaliados)
Apesar de sua utilidade, o defer tem algumas armadilhas que podem levar a comportamentos inesperados. Duas das mais comuns são o uso em loops e a avaliação antecipada de argumentos.
Armadilha do loop: Quando você usa defer dentro de um loop, as chamadas adiadas são empilhadas e executadas apenas no final da função, não a cada iteração. Isso pode causar acúmulo de recursos não liberados até o término da função. Por exemplo:
func processarArquivos(nomes []string) error {
for _, nome := range nomes {
f, err := os.Open(nome)
if err != nil {
return err
}
defer f.Close() // perigoso: arquivos só serão fechados no final da função
// processa o arquivo...
}
return nil
}Neste caso, todos os arquivos permanecem abertos até que a função retorne. Se houver muitos arquivos, isso pode esgotar os descritores de arquivo. A solução é mover o defer para uma função auxiliar ou fechar explicitamente dentro do loop.
Avaliação de argumentos: Os argumentos de uma função adiada são avaliados no momento em que o defer é registrado, não quando a função é executada. Isso pode causar surpresas se você espera que a variável mude antes da execução do defer:
func exemplo() {
x := 10
defer fmt.Println("x =", x) // imprime "x = 10"
x = 20
// a saída será "x = 10", não 20
}Para capturar o valor atualizado, você pode usar uma closure ou passar um ponteiro:
func exemplo() {
x := 10
defer func() {
fmt.Println("x =", x) // imprime "x = 20"
}()
x = 20
}Exemplos
Vamos consolidar o aprendizado com alguns exemplos práticos que ilustram os padrões e as armadilhas.
Exemplo 1: Liberação de recurso com mutex
package main
import (
"fmt"
"sync"
)
var contador int
var mu sync.Mutex
func incrementar() {
mu.Lock()
defer mu.Unlock()
contador++
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
incrementar()
}()
}
wg.Wait()
fmt.Println("Contador:", contador)
}Exemplo 2: Fechamento de arquivo com tratamento de erro
package main
import (
"fmt"
"io/ioutil"
"os"
)
func lerArquivoSeguro(nome string) ([]byte, error) {
f, err := os.Open(nome)
if err != nil {
return nil, err
}
defer f.Close()
dados, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return dados, nil
}
func main() {
dados, err := lerArquivoSeguro("exemplo.txt")
if err != nil {
fmt.Println("Erro:", err)
return
}
fmt.Println(string(dados))
}Exemplo 3: Armadilha do loop corrigida
package main
import (
"fmt"
"os"
)
func processarArquivos(nomes []string) error {
for _, nome := range nomes {
if err := processarArquivo(nome); err != nil {
return err
}
}
return nil
}
func processarArquivo(nome string) error {
f, err := os.Open(nome)
if err != nil {
return err
}
defer f.Close()
// processa o arquivo...
fmt.Println("Processado:", nome)
return nil
}
func main() {
nomes := []string{"a.txt", "b.txt"}
if err := processarArquivos(nomes); err != nil {
fmt.Println("Erro:", err)
}
}Exemplo 4: Avaliação de argumentos com closure
package main
import "fmt"
func main() {
x := 10
defer fmt.Println("x (avaliado):", x) // imprime 10
defer func() {
fmt.Println("x (closure):", x) // imprime 20
}()
x = 20
fmt.Println("x atual:", x) // imprime 20
}Boas práticas e observações finais
Sempre coloque o defer o mais próximo possível da aquisição do recurso. Isso torna o código mais legível e seguro. Evite usar defer dentro de loops longos; prefira funções auxiliares. Lembre-se de que defer tem um pequeno custo de desempenho, mas na maioria dos casos é insignificante. Use closures quando precisar capturar o valor atual de variáveis no momento da execução do defer.
Referências
- Effective Go: defer
- Go Specification: Defer statements
- Defer, Panic, and Recover (Blog post)
- Package sync - Mutex
- Package os - File.Close
- Package io/ioutil
Exercícios
Crie uma função que abra um arquivo, leia seu conteúdo e retorne os dados, garantindo que o arquivo seja fechado usando
defer. Teste com um arquivo existente e com um inexistente.✓ Resposta:func lerArquivo(nome string) ([]byte, error) { f, err := os.Open(nome) if err != nil { return nil, err } defer f.Close() dados, err := ioutil.ReadAll(f) if err != nil { return nil, err } return dados, nil }Escreva um programa que usa um mutex para proteger um contador compartilhado entre 10 goroutines, usando
deferpara liberar o mutex. Execute e verifique o resultado.✓ Resposta:package main import ( "fmt" "sync" ) var contador int var mu sync.Mutex func incrementar() { mu.Lock() defer mu.Unlock() contador++ } func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 100; j++ { incrementar() } }() } wg.Wait() fmt.Println("Contador:", contador) // Deve ser 1000 }O código a seguir tem um problema: ele usa
deferdentro de um loop. Identifique o problema e reescreva o código para corrigi-lo.func copiarArquivos(origens []string, destino string) error { for _, origem := range origens { f, err := os.Open(origem) if err != nil { return err } defer f.Close() // copia o arquivo... } return nil }✓ Resposta:O problema é que os arquivos só serão fechados no final da função, causando acúmulo de descritores abertos. A correção é mover a abertura e fechamento para uma função auxiliar:
func copiarArquivos(origens []string, destino string) error { for _, origem := range origens { if err := copiarArquivo(origem, destino); err != nil { return err } } return nil } func copiarArquivo(origem, destino string) error { f, err := os.Open(origem) if err != nil { return err } defer f.Close() // copia o arquivo... return nil }Explique por que o código a seguir imprime "0" em vez de "1" e corrija-o para imprimir "1".
func main() { x := 0 defer fmt.Println(x) x = 1 }✓ Resposta:O
deferavalia seus argumentos imediatamente, entãoxé avaliado como 0 no momento do registro. Para imprimir o valor final, use uma closure:func main() { x := 0 defer func() { fmt.Println(x) }() x = 1 }Crie uma função que simule uma transação de banco de dados: inicie uma transação, execute algumas operações e, se ocorrer um erro, faça rollback; caso contrário, faça commit. Use
deferpara garantir que a transação seja encerrada.✓ Resposta:type Transacao struct { // campos } func (t *Transacao) Executar(operacoes func() error) error { t.iniciar() var err error defer func() { if err != nil { t.rollback() } else { t.commit() } }() err = operacoes() return err } func (t *Transacao) iniciar() { /* ... */ } func (t *Transacao) rollback() { /* ... */ } func (t *Transacao) commit() { /* ... */ }