O problema do cleanup
Funções que adquirem recursos — abrem arquivos, travam mutexes, estabelecem conexões com banco de dados — precisam liberá-los ao terminar. A chamada de liberação deve sempre acontecer, independente de como a função sai: retorno normal, retorno antecipado por erro ou uma falha inesperada.
Sem um mecanismo dedicado, garantir o cleanup é manual e propenso a erros. Cada return antecipado precisa da sua própria chamada de liberação correspondente, e adicionar um novo caminho de saída significa revisar todos os existentes.
O statement defer do Go resolve isso diretamente.
O que defer faz
Um statement defer agenda uma chamada de função para ser executada logo antes da função ao redor retornar. A chamada é registrada imediatamente quando a linha defer é alcançada, mas a execução em si é adiada:
Saída:
start
end
deferred
A chamada adiada executa por último, depois que o restante de main terminou. À primeira vista parece simples — mas a regra de timing tem mais profundidade do que aparenta.
Quando uma função adiada executa
Uma função adiada tem garantia de execução em três condições:
- A função ao redor retorna — um statement
returnem qualquer ponto da função dispara as chamadas adiadas antes de devolver o controle para quem chamou. - A execução alcança o final do corpo da função — um return implícito na chave de fechamento.
- A função entra em panic — mesmo um panic não recuperado vai esvaziar a pilha de chamadas adiadas antes de se propagar.
As três condições compartilham a mesma garantia: a função adiada executa antes que a função ao redor devolva o controle. Seja qual for a forma de saída — limpa, antecipada ou por falha — as chamadas adiadas não são puladas.
Argumentos são avaliados imediatamente
Quando defer registra uma chamada, ele avalia os argumentos da função naquele momento, não quando a chamada adiada eventualmente executar:
Saída:
20
10
fmt.Println(x) é registrado com x = 10. Mesmo que x seja alterado para 20 depois, a chamada adiada ainda imprime 10. O argumento foi congelado no momento em que o statement defer foi avaliado.
Variáveis capturadas vs argumentos avaliados
Argumentos são avaliados de forma antecipada, mas variáveis capturadas por uma closure não são. Se você usar uma closure em um defer, ela verá o valor da variável no momento em que executar, não quando foi registrada:
Escolha entre as duas formas deliberadamente — elas se comportam de maneira diferente.
Múltiplos defers executam em ordem LIFO
Quando uma função registra mais de uma chamada adiada, elas executam em ordem last-in, first-out — como uma pilha. O último defer registrado executa primeiro:
Saída:
third
second
first
A ordem LIFO é intencional. Ela espelha a estrutura natural de aquisição de recursos: se você abre A e depois abre B, a ordem correta de cleanup é fechar B primeiro e depois A. Chamadas adiadas respeitam naturalmente esse aninhamento.
Defer e valores de retorno
O timing exato de uma chamada adiada é preciso: ela executa depois que o statement return define os parâmetros de resultado, mas antes que a função retorne para quem a chamou. Para funções com named return values, isso significa que uma função adiada pode ler e modificar o valor que será de fato retornado:
O statement return define n como 5. A função adiada então executa, multiplica n por 2, deixando-o em 10. Quem chamou recebe 10, não 5.
Essa interação só se aplica a named return values. Com valores de retorno sem nome, a função adiada não tem como alcançar o valor de retorno:
A distinção importa quando uma função adiada precisa ajustar o que será retornado — use named return values para tornar isso possível.
Limpando recursos
O uso mais comum de defer é garantir que um recurso seja liberado quando a função que o abriu sair. O exemplo canônico é um arquivo:
f.Close() é registrado imediatamente após o arquivo ser aberto com sucesso. Não importa quantos statements return venham depois — um para cada possível erro, ou o retorno final de sucesso — f.Close() executa exatamente uma vez, logo antes de processFile devolver o controle.
O mesmo padrão se aplica a qualquer recurso com um par de aquisição/liberação:
Cada defer é colocado logo ao lado da aquisição que protege. O código se lê na ordem natural das operações, e o cleanup é garantido sem precisar repetir chamadas de liberação ao longo da função.
Defer e panics
Um panic no Go desfaz a pilha de chamadas, executando as funções adiadas ao longo do caminho. Isso significa que defer é também o gancho para recuperação de um panic — uma função adiada pode chamar recover() para interceptar o panic e evitar que o programa encerre abruptamente.
Panic e recovery serão abordados em seu próprio artigo. Por ora, basta saber que defer está envolvido: qualquer cleanup registrado com defer ainda executará mesmo quando uma função entrar em panic, o que o torna o lugar certo para tratar tanto saídas normais quanto falhas inesperadas.