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

Exercícios

  1. 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
    }
  2. 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.
  3. 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
        }()
    }
  4. 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
    }
  5. 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.