A linguagem Go tem muitas das palavras-chave comuns a outras linguagens, tais como if
, switch
, for
etc. Uma palavra-chave que não existe na maioria das outras linguagens de programação é defer
e, embora seja menos comum, você verá o quão útil ela pode ser nos seus programas.
Um dos principais usos de uma instrução defer
é o da limpeza de recursos, como arquivos abertos, conexões de rede e conexões de banco de dados. Quando seu programa for finalizado com esses recursos, é importante fechá-los para evitar exaurir os limites do programa e permitir que outros programas acessem esses recursos. O defer
faz com que nosso fique mais limpo e menos suscetível a erros, mantendo as chamadas para fechar o arquivo/recurso próximas da chamada aberta.
Neste artigo, vamos aprender como usar a instrução defer para
a limpeza de recursos, além de vários erros comuns que são produzidos ao usar a defer
.
defer
Uma instrução defer
adiciona a chamada da função após a palavra-chave defer
em uma pilha. Todas as chamadas naquela pilha são chamadas quando a função na qual foram adicionadas retorna. Como as chamadas são colocadas em uma pilha, elas são chamadas na ordem do método de último a entrar, primeiro a sair.
Vamos ver como a defer
funciona imprimindo um pouco de texto:
package main
import "fmt"
func main() {
defer fmt.Println("Bye")
fmt.Println("Hi")
}
Na função main
, temos duas instruções. A primeira instrução começa com a palavra-chave defer
, seguida de uma instrução print
que imprime Bye
. A próxima linha imprime Hi
.
Se executarmos o programa, vamos ver o seguinte resultado:
OutputHi
Bye
Note que o Hi
foi impresso primeiro. Isso acontece porque qualquer instrução precedida pela palavra-chave defer
não é invocada até o final da função na qual a defer
tiver sido usada.
Vamos dar outra olhada no programa e, desta vez, vamos adicionar alguns comentários para ajudar a ilustrar o que está acontecendo:
package main
import "fmt"
func main() {
// defer statement is executed, and places
// fmt.Println("Bye") on a list to be executed prior to the function returning
defer fmt.Println("Bye")
// The next line is executed immediately
fmt.Println("Hi")
// fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}
A chave para entender a defer
está no fato de que, quando a instrução defer
é executada, os argumentos relacionados à função adiada são prontamente avaliados. Quando uma defer
executa, ela coloca a instrução depois de si mesma em uma lista para ser invocada antes do retorno da função.
Embora esse código ilustre a ordem na qual a defer
seria executada, não é uma maneira típica que seria usada ao se escrever um programa em Go. É mais provável que estejamos usando a defer
para limpar um recurso, como um identificador de arquivo. Vamos ver como fazer isso a seguir.
defer
para limpar recursosUsar defer
para limpar recursos é muito comum em Go. Vamos olhar primeiro um programa que grava uma string em um arquivo, mas não usa a defer
para lidar com a limpeza de recursos:
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
_, err = io.WriteString(file, text)
if err != nil {
return err
}
file.Close()
return nil
}
Neste programa, há uma função chamada write
que primeiro tentará criar um arquivo. Se a função tiver um erro, o programa retornará o erro e sairá da função. Em seguida, a função tenta gravar a string This is a readme file
no arquivo especificado. Se a função receber um erro, o programa retornará o erro e sairá da função. Em seguida, a função tentará fechar o arquivo e liberar o recurso de volta para o sistema. Por fim, a função retorna nil
para indicar que a função foi executada sem erros.
Embora este código funcione, há um bug sutil. Se a chamada para io.WriteString
falhar, a função retornará sem fechar o arquivo e sem liberar o recurso de volta ao sistema.
Poderíamos resolver esse problema adicionando outra instrução file.Close()
, que é a maneira como você provavelmente resolveria isso em uma linguagem sem defer
:
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
_, err = io.WriteString(file, text)
if err != nil {
file.Close()
return err
}
file.Close()
return nil
}
Agora, mesmo se a chamada para io.WriteString
falhar, ainda fecharemos o arquivo. Embora este seja um bug relativamente fácil de se detectar e corrigir, com uma função mais complicada, ele poderia passar despercebido.
Em vez de de adicionar a segunda chamada a file.Close()
, podemos usar uma instruçãodefer
para garantir que, independentemente de quais ramificações sejam tomados durante a execução, sempre chamaremos Close()
.
Aqui está a versão que usa a palavra-chave defer
:
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err
}
return nil
}
Desta vez, adicionamos a linha de código: defer file.Close()
. Isso diz ao compilador que ele deve executar o file.Close
antes de sair da função write
.
Agora, garantimos que mesmo se nós adicionarmos mais código e criarmos outra ramificação que saia da função no futuro, iremos sempre limpar e fechar o arquivo.
No entanto, ao adicionarmos a defer, introduzimos um novo bug. Já não iremos mais verificar o possível erro que pode ser retornado do método Close
. Isso acontece porque quando usamos defer
, não há como comunicar qualquer valor de retorno de volta para nossa função.
Em Go, é considerado uma prática segura e aceita chamar Close()
mais de uma vez sem afetar o comportamento de seu programa. Se Close()
for retornar um erro, ela fará isso na primeira vez que for chamada. Isso nos permite chamá-la de maneira explícita no caminho bem-sucedido da execução em nossa função.
Vamos ver como podemos tanto usar tanto a defer
quanto a chamada para Close
e ainda reportar um erro se encontrarmos um.
package main
import (
"io"
"log"
"os"
)
func main() {
if err := write("readme.txt", "This is a readme file"); err != nil {
log.Fatal("failed to write file:", err)
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err
}
return file.Close()
}
A única mudança neste programa é a última linha na qual retornamos file.Close()
. Se a chamada para Close
resultar em um erro, isso agora será retornado conforme esperado para a função de chamada. Lembre-se de que nossa instrução defer file.Close()
também será executada após a instrução return
. Isso significa que file.Close()
é possivelmente chamada duas vezes. Embora isso não seja o ideal, é uma prática aceitável, já que não deve criar qualquer efeito colateral em seu programa.
Se, no entanto, recebermos um erro mais cedo na função, como quando chamamos WriteString
, a função retornará aquele erro e também tentará chamar file.Close
porque ela foi adiada. Embora o file.Close
também possa (e provavelmente irá) retornar um erro também, ele não é mais algo que nos preocupe, uma vez que recebemos um erro que muito provavelmente nos dirá o que deu errado, para início de conversa.
Até agora, vimos o modo como podemos usar uma única defer
para garantir que limpamos nossos recursos corretamente. Em seguida, veremos como podemos usar várias instruções defer
para limpar mais de um recurso.
defer
É normal ter mais de uma instrução defer
em uma função. Vamos criar um programa que tenha apenas instruções defer
nele para ver o que acontece quando introduzimos várias defers:
package main
import "fmt"
func main() {
defer fmt.Println("one")
defer fmt.Println("two")
defer fmt.Println("three")
}
Se executarmos o programa, vamos receber o seguinte resultado:
Outputthree
two
one
Note que a ordem é a oposta àquela em que chamamos as instruções defer
. Isso acontece porque cada instrução adiada que é chamada é empilhada no topo da anterior; em seguida, ela é chamada na ordem reversa, quando a função sai do escopo (Última a entrar, primeira a sair).
Você pode ter tantas chamadas adiadas quantas forem necessárias em uma função. É importante lembrar, porém, que todas elas serão chamadas na ordem oposta em que tiverem sido executadas.
Agora que entendemos a ordem em que várias defers serão executados, vamos ver como usaríamos várias defers para limpar vários recursos. Criaremos um programa que abre um arquivo, grava nele e então o abre novamente para copiar o conteúdo para outro arquivo.
package main
import (
"fmt"
"io"
"log"
"os"
)
func main() {
if err := write("sample.txt", "This file contains some sample text."); err != nil {
log.Fatal("failed to create file")
}
if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
log.Fatal("failed to copy file: %s")
}
}
func write(fileName string, text string) error {
file, err := os.Create(fileName)
if err != nil {
return err
}
defer file.Close()
_, err = io.WriteString(file, text)
if err != nil {
return err
}
return file.Close()
}
func fileCopy(source string, destination string) error {
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
dst, err := os.Create(destination)
if err != nil {
return err
}
defer dst.Close()
n, err := io.Copy(dst, src)
if err != nil {
return err
}
fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)
if err := src.Close(); err != nil {
return err
}
return dst.Close()
}
Nós adicionamos uma nova função chamada fileCopy
. Nessa função, abrimos primeiro nosso arquivo fonte do qual vamos copiar. Verificamos, então, para saber se recebemos um erro ao abrir o arquivo. Se recebemos um erro, temos que return
(retornar) o erro e sair da função. Caso contrário, teremos que defer
(adiar) o fechamento do arquivo fonte que acabamos de abrir.
Em seguida, criamos o arquivo de destino. Novamente, verificamos para saber se recebemos um erro ao criar o arquivo. Caso isso aconteça, temos que return
(retornar) aquele erro e sair da função. Caso contrário, teremos também que defer
(adiar) o Close()
em relação ao arquivo de destino. Agora, temos duas funções defer
que serão chamadas quando a função sair do seu escopo.
Agora que temos ambos os arquivos abertos, vamos usar Copy()
para os dados do arquivo fonte para o arquivo de destino. Se isso for bem-sucedido, tentaremos fechar ambos os arquivos. Se recebermos um erro ao tentar fechar qualquer um dos arquivos, teremos que return
(retornar) o erro e sair do escopo da função.
Note que chamamos explicitamente Close()
para cada arquivo, embora a defer
também irá chamar Close()
. Isso é para garantir que, se houver um erro ao fechar um arquivo, reportemos esse erro. Também fica garantido que se, por qualquer razão, a função sair precocemente com um erro, como por exemplo, se deixássemos de copiar entre os dois arquivos, cada arquivo ainda tentará fechar corretamente a partir das chamadas adiadas.
Neste artigo, aprendemos sobre a instrução defer
e como ela pode ser usada para garantir a limpeza correta dos recursos do sistema em nosso programa. A limpeza adequada dos recursos do sistema fará com que seu programa use menos memória e tenha um melhor desempenho. Para aprender mais sobre onde a defer
é usada, leia o artigo sobre Como lidar com emergências, ou explore toda a nossa série Como programar em Go.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
Gopher Guides is a training and consulting company specializing in Go and Go related technologies. Co-founder:s Mark Bates & Cory LaNou.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.
Full documentation for every DigitalOcean product.
The Wave has everything you need to know about building a business, from raising funding to marketing your product.
Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.
New accounts only. By submitting your email you agree to our Privacy Policy
Scale up as you grow — whether you're running one virtual machine or ten thousand.
Sign up and get $200 in credit for your first 60 days with DigitalOcean.*
*This promotional offer applies to new accounts only.