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.
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!
Parabéns pela explicação, está excelente.