Menu

defer em Golang: ordem, avaliação dos argumentos e armadilhas

defer agenda uma chamada para executar quando a função que a contém retorna. Veja a ordem LIFO, quando os argumentos são avaliados, como fechar arquivos e liberar mutexes, defer em laços e como alterar resultados nomeados.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

O que o defer faz

defer empilha uma chamada de função em uma lista. Quando a função que a contém retorna, a lista executa em ordem inversa.

Saída:

start
end
deferred 3
deferred 2
deferred 1

A ordem é a última a entrar, a primeira a sair, como uma pilha. Isso combina com o jeito como os recursos se aninham: se você abre A e depois B, normalmente quer fechar B antes de A.

Limpeza ao lado da aquisição

O uso principal do defer é escrever a limpeza na linha logo depois da aquisição, para que nenhum caminho de retorno possa esquecê-la.

Repare na ordem: verifique o erro primeiro, depois adie. Se os.Open falhou, f é nil, e adiar f.Close() antes da verificação chamaria Close em um *os.File nil (o que devolve um erro que você nunca vê, e confunde quem lê).

O mesmo formato funciona para locks:

mu.Lock()
defer mu.Unlock()

Se o código entre eles causar panic, o mutex ainda é liberado.

Os argumentos são avaliados na hora

A função adiada e os seus argumentos são avaliados quando a instrução defer executa. Só a chamada em si espera.

Saída:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) capturou o valor 1 na linha do defer. A closure não tem argumentos; ela lê x quando finalmente executa. Escolha a forma que corresponde ao que você quer registrar.

Isso também vale para receivers de métodos. defer t.Stop() avalia t na hora, então reatribuir t depois não muda qual valor será parado.

Um truque comum de medição de tempo usa essa regra de propósito:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") é chamada na hora (ela pode imprimir "enter" e registrar o horário de início), e a função que ela devolve é a que fica adiada.

defer em um laço

Chamadas adiadas executam quando a função retorna, não no fim de cada iteração do laço. Em um laço sobre muitos arquivos, isso mantém todos os arquivos abertos até a função terminar.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

Com milhares de caminhos, isso esgota os descritores de arquivo. Mova o corpo para uma função própria, para que cada defer execute a cada iteração:

Cada chamada a processFile fecha o seu arquivo antes de o próximo ser aberto. Um literal de função chamado na hora (func() { ... }()) funciona do mesmo jeito quando uma função auxiliar nomeada parece exagero.

Alterando valores de retorno

Uma closure adiada executa depois de a instrução return atribuir os resultados, e pode modificar resultados nomeados antes que quem chama os veja.

Isto imprime 10 e save failed: disk full. Com um resultado sem nome, uma função adiada ainda executa, mas não tem como mudar o que é devolvido.

Capturando o erro do Close

defer f.Close() joga fora o erro do Close. Para arquivos que você só lê, tudo bem. Para arquivos em que você escreveu, o Close pode reportar uma falha na gravação final, então o erro importa. Um resultado nomeado permite guardá-lo:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) é uma alternativa mais curta quando você quer os dois erros reportados.

defer, panic e recover

Chamadas adiadas executam enquanto um panic desenrola a pilha. Esse é o único lugar em que recover tem efeito, e é assim que um servidor evita que uma requisição ruim derrube o processo. Os detalhes estão em panic e recover.

Chamadas adiadas não executam quando o programa sai por os.Exit ou log.Fatal. Se main adia uma limpeza e depois chama os.Exit(1), a limpeza é pulada.

Custo

Desde o Go 1.14, a maioria dos defers é gerada em linha pelo compilador (open-coded) e custa poucos nanossegundos. Usar defer em toda liberação de mutex e fechamento de arquivo é o estilo normal. Defers dentro de laços são a exceção: eles não podem ser gerados em linha e caem em um caminho mais lento, mais um motivo para mover o corpo de laços para funções.

Erros comuns

  • Adiar antes de verificar o erro. Verifique o err do Open primeiro, depois faça defer Close.
  • Esperar limpeza a cada iteração de um laço. Defers executam na saída da função.
  • Esperar que um argumento adiado veja mudanças posteriores. Os argumentos ficam fixos na linha do defer. Use uma closure para ler na saída.
  • Contar com o defer junto com os.Exit. Ele nunca executa.

Perguntas frequentes

O que o defer faz em Go?

defer f() agenda f() para executar quando a função que a contém retornar, seja normalmente, por um return antecipado ou por um panic. Ele serve para colocar a limpeza (fechar um arquivo, liberar um mutex) logo ao lado do código que adquiriu o recurso.

Em que ordem as chamadas adiadas executam em Go?

A última a entrar é a primeira a sair (LIFO). A chamada adiada mais recente executa primeiro. defer fmt.Println(1); defer fmt.Println(2) imprime 2 e depois 1.

Quando os argumentos de uma função adiada são avaliados?

Na hora, quando a instrução defer executa, e não quando a chamada roda. x := 1; defer fmt.Println(x); x = 2 imprime 1. Para ler o valor no momento da saída, adie uma closure: defer func() { fmt.Println(x) }().

O defer executa em um panic ou em os.Exit?

Chamadas adiadas executam enquanto um panic desenrola a pilha, e é por isso que recover funciona dentro delas. Elas não executam quando o programa chama os.Exit (ou log.Fatal, que o chama), e não executam em outras goroutines quando main retorna.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR