Carregando
Todos os artigos
Programação 14 min de leitura

Como Adiar a Execução de Funções com defer em Go: Um Guia Visual e Prático

Aprenda o defer do Go de forma visual: a pilha em que o último a entrar sai primeiro, quando as chamadas adiadas rodam de verdade, argumentos avaliados na hora, fechar arquivos e destravar mutexes, recuperar de pânicos, mudar o resultado, o os.Exit, um benchmark real e exercícios.

nbkhope
Nesta página

O Go roda os seus comandos em ordem, de cima para baixo. Mas algum trabalho pertence ao fim: fechar um arquivo que você abriu, destravar uma trava que você pegou, registrar que uma função terminou. A palavra-chave defer ("adiar") do Go deixa você escrever essa limpeza logo ao lado da preparação e fazê-la rodar automaticamente quando a função retornar, seja como for que ela retorne.

Neste guia você vai ver a ordem em que as chamadas adiadas rodam, exatamente quando elas rodam, um detalhe surpreendente sobre os argumentos delas e os padrões do mundo real: fechar arquivos, destravar mutexes, recuperar de pânicos e até mudar o resultado de uma função. Você também vai ver o único jeito de pular as chamadas adiadas, e um benchmark do quanto o defer custa. Exercícios fecham o artigo.

Todas as saídas abaixo vêm de execuções reais com o Go 1.22 no Windows.

Adiando uma chamada para depois

1package main
2
3import "fmt"
4
5func main() {
6	fmt.Println("Oi")
7	fmt.Println("Tudo bem?")
8}
Oi
Tudo bem?

Agora coloque defer na frente da primeira chamada:

1package main
2
3import "fmt"
4
5func main() {
6	defer fmt.Println("Oi")
7	fmt.Println("Tudo bem?")
8}
Tudo bem?
Oi

O defer não roda a chamada agora. Ele a agenda para rodar quando a função em volta retornar, aqui quando a main terminar.

O último a entrar é o primeiro a sair

Adie várias chamadas e elas rodam na ordem inversa:

1package main
2
3import "fmt"
4
5func main() {
6	defer fmt.Println("1")
7	defer fmt.Println("2")
8	defer fmt.Println("3")
9	fmt.Println("a main está quase retornando")
10}
1a main está quase retornando
23
32
41
O defer funciona como uma pilha de pratos. Enquanto a main roda, cada comando defer empilha uma chamada na pilha de defers da função: primeiro Println("1"), depois Println("2") por cima, depois Println("3") por cima desse. Quando a main retorna, o Go tira as chamadas do topo uma de cada vez: o 3 roda primeiro, depois o 2, depois o 1. O último a entrar é o primeiro a sair. Essa ordem é exatamente o que a limpeza precisa: os recursos são liberados na ordem inversa em que foram pegos

Cada função tem a sua própria pilha de defers (stack). Cada defer empilha uma chamada no topo; quando a função retorna, o Go as tira do topo: o último a entrar é o primeiro a sair (last-in, first-out, LIFO). É exatamente a ordem de que a limpeza precisa: se você abre um banco de dados e depois um arquivo, quer fechar o arquivo primeiro e o banco por último.

Quando exatamente uma chamada adiada roda?

Quando a função retorna, e não quando o bloco ou o laço atual termina:

1package main
2
3import "fmt"
4
5func ajudante() {
6	defer fmt.Println("ajudante: chamada adiada")
7	fmt.Println("ajudante: trabalhando")
8}
9
10func main() {
11	fmt.Println("main: antes do ajudante")
12	ajudante()
13	fmt.Println("main: depois do ajudante")
14
15	for passo := 1; passo <= 3; passo++ {
16		defer fmt.Println("defer do laço", passo)
17	}
18	fmt.Println("main: laço terminou")
19}
1main: antes do ajudante
2ajudante: trabalhando
3ajudante: chamada adiada
4main: depois do ajudante
5main: laço terminou
6defer do laço 3
7defer do laço 2
8defer do laço 1
Quando as chamadas adiadas rodam, numa linha do tempo. A main chama o ajudante; dentro do ajudante, o Println adiado espera até o ajudante retornar e roda antes de o controle voltar para a main. No laço da main, três comandos defer rodam durante o laço, mas as chamadas esperam: elas se acumulam na pilha da main e só rodam quando a main retorna, depois de main: laço terminou, na ordem inversa: 3, 2, 1. As chamadas adiadas pertencem à função, e não ao bloco ou ao laço

A chamada adiada do ajudante rodou assim que o ajudante retornou. Mas as três chamadas adiadas dentro do laço se acumularam até a main retornar. Isso importa quando um laço abre muitos arquivos: adiar o Close dentro do laço mantém todos os arquivos abertos até a função inteira terminar. Mova o corpo do laço para uma função própria, e cada Close adiado roda no fim de cada volta.

Os argumentos são avaliados na hora

Isto surpreende quase todo mundo:

1package main
2
3import "fmt"
4
5func main() {
6	contagem := 1
7	defer fmt.Println("adiada com o valor:", contagem)
8
9	defer func() {
10		fmt.Println("a closure adiada vê:", contagem)
11	}()
12
13	contagem = 99
14	fmt.Println("agora a contagem é", contagem)
15}
agora a contagem é 99
a closure adiada vê: 99
adiada com o valor: 1
Quando os argumentos de uma chamada adiada são avaliados? defer fmt.Println("adiada com o valor:", contagem) avalia os argumentos na linha do defer, quando a contagem vale 1, e guarda o valor 1 junto com a chamada, então imprime 1 mesmo que a contagem vire 99 depois. Uma closure adiada, defer func() { fmt.Println(contagem) }(), não tem argumentos: ela lê a contagem quando finalmente roda, depois de contagem = 99, então imprime 99. A chamada acontece depois; os argumentos são avaliados agora

A chamada acontece depois, mas os argumentos dela são avaliados na linha do defer, então a contagem foi capturada como 1. Uma closure adiada (uma func() { … }() anônima) não tem argumentos: ela lê a contagem quando finalmente roda, e vê 99. Escolha o comportamento de que você precisa.

Uso real 1: fechar arquivos

O padrão clássico: abra algo e imediatamente adie o fechamento:

1package main
2
3import (
4	"fmt"
5	"os"
6)
7
8func escreverNotas(nomeDoArquivo string) error {
9	arquivo, err := os.Create(nomeDoArquivo)
10	if err != nil {
11		return err
12	}
13	defer arquivo.Close()
14
15	fmt.Fprintln(arquivo, "Comprar leite")
16	fmt.Fprintln(arquivo, "Ligar para a Ana")
17	return nil
18}
19
20func main() {
21	if err := escreverNotas("notas.txt"); err != nil {
22		fmt.Println("erro:", err)
23		return
24	}
25	defer os.Remove("notas.txt")
26
27	conteudo, err := os.ReadFile("notas.txt")
28	if err != nil {
29		fmt.Println("erro:", err)
30		return
31	}
32	fmt.Print(string(conteudo))
33}
Comprar leite
Ligar para a Ana

Escrever defer arquivo.Close() logo depois de um os.Create bem-sucedido quer dizer que você não tem como esquecer, mesmo que a função ganhe mais returns depois. (err é o nome convencional do Go para um valor de erro; conferir if err != nil depois de cada chamada que pode falhar é o padrão do Go.) O defer os.Remove apaga o arquivo de exemplo quando a main termina.

Uso real 2: destravar um mutex

Quando várias goroutines (as "threads" leves do Go) dividem dados, um mutex garante que só uma mexa neles por vez. O defer garante o destravamento:

1package main
2
3import (
4	"fmt"
5	"sync"
6)
7
8type Contador struct {
9	trava sync.Mutex
10	valor int
11}
12
13func (contador *Contador) Incrementar() {
14	contador.trava.Lock()
15	defer contador.trava.Unlock()
16	contador.valor++
17}
18
19func main() {
20	contador := &Contador{}
21	var grupo sync.WaitGroup
22	for trabalhador := 0; trabalhador < 100; trabalhador++ {
23		grupo.Add(1)
24		go func() {
25			defer grupo.Done()
26			contador.Incrementar()
27		}()
28	}
29	grupo.Wait()
30	fmt.Println("valor final:", contador.valor)
31}
valor final: 100

100 goroutines somam 1 cada uma, e o resultado é sempre 100. Se o Incrementar retornasse antes ou entrasse em pânico sem destravar, todas as outras goroutines esperariam para sempre. Com o defer, o destravamento sempre acontece. (O defer grupo.Done() é a mesma ideia: avisa o WaitGroup que esta goroutine terminou, aconteça o que acontecer.)

Uso real 3: recuperar de um pânico

Um pânico (panic) é a parada de emergência do Go (como o erro "index out of range" que você talvez já tenha visto). As chamadas adiadas continuam rodando durante um pânico, e uma função adiada pode chamar recover() para parar o pânico e transformá-lo num erro comum:

1package main
2
3import "fmt"
4
5func elementoSeguro(numeros []int, indice int) (valor int, err error) {
6	defer func() {
7		if problema := recover(); problema != nil {
8			err = fmt.Errorf("recuperado: %v", problema)
9		}
10	}()
11	return numeros[indice], nil
12}
13
14func main() {
15	numeros := []int{10, 20, 30}
16	fmt.Println(elementoSeguro(numeros, 1))
17	fmt.Println(elementoSeguro(numeros, 5))
18	fmt.Println("o programa continua")
19}
20 <nil>
0 recuperado: runtime error: index out of range [5] with length 3
o programa continua

O recover() só funciona dentro de uma função adiada. Use com moderação, em fronteiras como o tratador de pedidos de um servidor web, para que um pedido ruim não derrube o servidor inteiro. Erros comuns devem ser devolvidos como valores error.

Uso real 4: mudar o resultado

Olhe de novo o elementoSeguro: os resultados dele têm nome (valor int, err error), e a função adiada mudou o err depois que o comando return já o tinha definido. Uma função adiada roda depois que o return define os resultados, mas antes de quem chamou recebê-los, então ela pode ajustá-los:

1package main
2
3import "fmt"
4
5func dobro(numero int) (resultado int) {
6	defer func() {
7		resultado *= 2
8	}()
9	return numero + 1
10}
11
12func main() {
13	fmt.Println(dobro(4))
14}
10

O return numero + 1 põe 5 no resultado, e depois a função adiada o dobra para 10. Útil para acrescentar contexto a erros; confuso se exagerado.

A única coisa que pula os defers: os.Exit

1package main
2
3import (
4	"fmt"
5	"os"
6)
7
8func main() {
9	defer fmt.Println("isto nunca é impresso")
10	fmt.Println("saindo")
11	os.Exit(0)
12}
saindo

O os.Exit termina o programa na hora: as chamadas adiadas não rodam, então arquivos podem não ser gravados e a limpeza é pulada. (Um pânico que ninguém recupera roda as chamadas adiadas antes; o os.Exit, não.) Na main, prefira retornar normalmente, ou use o os.Exit só depois de terminar a limpeza.

Quanto o defer custa?

Versões antigas do Go deixavam o defer bem mais lento, e por isso parte do código o evitava. Medimos travar e destravar um mutex com e sem defer no Go 1.22:

Quanto o defer custa no Go 1.22, medido com testing.Benchmark no Windows, mediana de 7 execuções. Travar e destravar um mutex direto: 14,62 nanossegundos por chamada. O mesmo com defer trava.Unlock(): 15,16 nanossegundos, cerca de meio nanossegundo a mais, porque desde o Go 1.14 o compilador embute os defers simples, copiando a chamada adiada para cada retorno. Uma closure chamada dentro de um laço: 1,91 nanossegundo. A mesma closure adiada dentro de um laço, que não pode ser embutida: 26,29 nanossegundos, cerca de 24 nanossegundos a mais por chamada adiada. Use o defer à vontade para a limpeza; só laços muito quentes notam a diferença
O quêTempo por chamada
Travar e destravar direto14,62 ns
Travar e adiar o destravamento com defer15,16 ns
Uma closure chamada dentro de um laço1,91 ns
A mesma closure adiada dentro de um laço26,29 ns

Desde o Go 1.14, o compilador embute (open-codes) as chamadas adiadas simples, copiando-as para cada lugar em que a função retorna, então um defer comum custa cerca de meio nanossegundo (a diferença mudava de uma execução para outra). Só um defer dentro de um laço (em que o número de chamadas adiadas não é conhecido de antemão) pega o caminho mais lento: cerca de 24 nanossegundos por chamada adiada, comparado com chamar a mesma closure direto. Use o defer à vontade para a limpeza.

Sua vez: pratique

Tente fazer estes antes de espiar as respostas. Rode cada um com go run ..

Exercício 1: Adivinhe a saída (aquecimento). O que isto imprime?

1package main
2
3import "fmt"
4
5func main() {
6	defer fmt.Println("A")
7	fmt.Println("B")
8	defer fmt.Println("C")
9	fmt.Println("D")
10}

Exercício 2: Contagem regressiva. Escreva um laço de 1 a 5 que adie a impressão de cada número, seguido de uma linha que imprime Contando…, para a saída mostrar Contando… e depois 5, 4, 3, 2, 1.

Exercício 3: Rastreie uma função. Escreva um ajudante rastrear(nome string) func() que imprime entrou nome quando é chamado e devolve uma função que imprime saiu nome, para que defer rastrear("processar")() na primeira linha de uma função registre o começo e o fim dela.

Exercício 4: Adivinhe os argumentos. O que isto imprime, e por quê?

1package main
2
3import "fmt"
4
5func main() {
6	mensagem := "primeira"
7	defer fmt.Println(mensagem)
8	mensagem = "segunda"
9	defer func() { fmt.Println(mensagem) }()
10	mensagem = "terceira"
11}

Exercício 5: Divisão segura (desafio). Escreva divisaoSegura(numerador, denominador int) (resultado int, err error), que recupera do pânico "integer divide by zero" (divisão inteira por zero) e o devolve como erro.

Quiz rápido

  1. Em que ordem três chamadas adiadas rodam?
  2. Quando roda uma chamada adiada dentro de um laço?
  3. Quando os argumentos de uma chamada adiada são avaliados?
  4. As chamadas adiadas rodam durante um pânico? E durante um os.Exit?
  5. Por que colocar defer arquivo.Close() logo depois de abrir um arquivo?

Respostas

Exercício 1: B e D são impressos enquanto a main roda; depois as chamadas adiadas rodam na ordem inversa, o C antes do A:

1package main
2
3import "fmt"
4
5func main() {
6	defer fmt.Println("A")
7	fmt.Println("B")
8	defer fmt.Println("C")
9	fmt.Println("D")
10}
1B
2D
3C
4A

Exercício 2: As impressões adiadas se acumulam e rodam ao contrário depois do laço:

1package main
2
3import "fmt"
4
5func main() {
6	for numero := 1; numero <= 5; numero++ {
7		defer fmt.Println(numero)
8	}
9	fmt.Println("Contando…")
10}
1Contando…
25
34
43
52
61

Exercício 3: O rastrear("processar") roda na hora (imprimindo entrou), e a função que ele devolve é a que fica adiada:

1package main
2
3import "fmt"
4
5func rastrear(nome string) func() {
6	fmt.Println("entrou", nome)
7	return func() {
8		fmt.Println("saiu", nome)
9	}
10}
11
12func processar() {
13	defer rastrear("processar")()
14	fmt.Println("processando…")
15}
16
17func main() {
18	processar()
19}
entrou processar
processando…
saiu processar

Repare nos dois pares de parênteses: defer rastrear("processar")() adia a função devolvida. Escrever defer rastrear("processar") adiaria a chamada inteira do rastrear, imprimindo entrou no fim.

Exercício 4: A primeira chamada adiada capturou "primeira" como argumento; a closure lê a mensagem quando roda, depois de ela virar "terceira". A ordem inversa põe a closure primeiro:

1package main
2
3import "fmt"
4
5func main() {
6	mensagem := "primeira"
7	defer fmt.Println(mensagem)
8	mensagem = "segunda"
9	defer func() { fmt.Println(mensagem) }()
10	mensagem = "terceira"
11}
terceira
primeira

Exercício 5: Um recover adiado transforma o pânico num erro por meio do resultado com nome:

1package main
2
3import "fmt"
4
5func divisaoSegura(numerador, denominador int) (resultado int, err error) {
6	defer func() {
7		if problema := recover(); problema != nil {
8			err = fmt.Errorf("não dá para dividir: %v", problema)
9		}
10	}()
11	return numerador / denominador, nil
12}
13
14func main() {
15	fmt.Println(divisaoSegura(10, 2))
16	fmt.Println(divisaoSegura(10, 0))
17}
5 <nil>
0 não dá para dividir: runtime error: integer divide by zero

(Em código de verdade você simplesmente conferiria if denominador == 0 antes; este exercício é para ver o recover funcionando.)

Quiz: (1) Ao contrário: o último adiado é o primeiro a rodar. (2) Quando a função inteira em volta retorna, e não no fim de cada volta. (3) Na linha do defer; só a chamada em si espera. (4) Sim durante um pânico (é assim que o recover funciona); não durante o os.Exit, que termina o programa na hora. (5) Para o fechamento não poder ser esquecido, seja qual for o return que a função tomar depois.

Cola rápida

Eu quero…Escreva
Rodar uma chamada quando a função retornardefer fmt.Println("pronto")
Fechar um arquivoarquivo, err := os.Open(nome) … defer arquivo.Close()
Destravar um mutextrava.Lock() e depois defer trava.Unlock()
Marcar uma goroutine como terminadadefer grupo.Done()
Ler as variáveis no fim, e não agoradefer func() { … }()
Transformar um pânico num errodefer func() { if problema := recover(); problema != nil { err = … } }()
Mudar um resultado com nomedefer func() { resultado *= 2 }()
Rastrear uma funçãodefer rastrear("nome")()
Sair sem rodar os defersos.Exit(codigo) (com cuidado)

Resumindo

O defer mantém a preparação e a limpeza lado a lado:

  • Uma chamada adiada roda quando a função em volta retorna, com o último a entrar saindo primeiro.
  • Ela pertence à função, e não ao bloco: defers dentro de um laço se acumulam até a função terminar.
  • Os argumentos são avaliados na linha do defer; uma closure adiada lê as variáveis quando roda.
  • Usos clássicos: fechar arquivos, destravar mutexes, WaitGroup.Done e recover para transformar um pânico num erro.
  • Funções adiadas podem mudar resultados com nome, porque rodam depois que o return os define.
  • O os.Exit pula as chamadas adiadas; um pânico, não.
  • Quase não custa nada: 15,16 ns contra 14,62 ns para destravar um mutex no nosso benchmark; só defers dentro de laços custam mais (cerca de 24 ns cada).

Voltando pela série: Slices de Duas Dimensões em Go, O Laço For em Go e Seu Primeiro Programa em Go. A seguir: ponteiros em Go.

Obrigado pela leitura!

Conhece alguém que vai gostar? Mande para essa pessoa.

Mais artigos

Leia a seguir

Programação 5 min de leitura

Como Tratar Erros em Go: Um Guia Prático

Aprenda a tratar erros em Go com exemplos executáveis: retorne um erro, confira o resultado, acrescente contexto com %w e reconheça um arquivo ausente com errors.Is.

nbkhope

Comentários

Seja gentil e específico. Comentários com links de contas novas aparecem depois da análise de um moderador.