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

Exercícios

  1. 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
    }
  2. Escreva um programa que usa um mutex para proteger um contador compartilhado entre 10 goroutines, usando defer para 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
    }
  3. O código a seguir tem um problema: ele usa defer dentro 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
    }
  4. 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 defer avalia seus argumentos imediatamente, então x é 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
    }
  5. 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 defer para 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() { /* ... */ }