Entendendo o defer no Go

Introdução

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.

O que é uma instrução 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:

main.go

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:

main.go

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.

Usando defer para limpar recursos

Usar 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:

main.go

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:

main.go

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çãodeferpara 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:

main.go

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.

main.go

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.

Múltiplas instruções 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:

main.go

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.

main.go

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 %sn", 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.

Conclusão

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.