One place for hosting & domains

      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:

      Output

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

      Output

      three 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.



      Source link

      Información sobre defer en Go


      Introducción

      Go tiene muchas de las palabras claves de flujo de control comunes que se encuentran en otros lenguajes de programación, como if, switch y for, entre otras. Una palabra clave que no tienen la mayoría de los otros lenguajes de programación es defer, y aunque es menos común, pronto verá la utilidad de esta palabra en sus programas.

      Uno de los principales usos de una instrucción defer es el de limpiar recursos como archivos abiertos, conexiones de red y controladores de bases de datos. Cuando su programa termine con estos recursos, es importante cerrarlos para evitar agotar los límites del programa y permitir que otros programas accedan a esos recursos. defer aporta más claridad a nuestro código y reduce su propensión a experimentar errores mediante la conservación de las invocaciones para cerrar archivos y recursos cerca de las invocaciones abiertas.

      En este articulo, aprenderá a usar de forma adecuada la instrucción defer para limpiar recursos y también verá algunos errores comunes que se cometen cuando se utiliza defer.

      Qué es una instrucción defer

      Una instrucción defer añade la invocación de la función después de la palabra clave defer en una pila. Todas las invocaciones de la pila en cuestión se invocan cuando regresa la función en la que se añadieron. Debido a que las invocaciones se disponen en una pila, se llaman en el orden “último en entrar” y “primero en salir”.

      Veremos la forma en que defer funciona imprimiendo un texto:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("Bye")
          fmt.Println("Hi")
      }
      

      En la función main hay dos instrucciones. La primera comienza con la palabra clave defer y le sigue una afirmación print que imprime Bye. La siguiente línea imprime Hi.

      Si ejecutamos el programa, veremos el siguiente resultado:

      Output

      Hi Bye

      Observe que Hi se imprimió primero. Esto es porque cualquier instrucción precedida por la palabra clave defer no se invoca hasta el final de la función en la cual se utilizó defer.

      Echaremos otro vistazo al programa y esta vez añadiremos algunos comentarios para ayudar a ilustrar lo que está sucediendo:

      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
      }
      

      La clave para comprender defer es que cuando se ejecuta la instrucción defer, los argumentos para la función diferida se evalúan de inmediato. Cuando defer se ejecuta, dispone la instrucción después de sí en una lista para que se invoque antes del regreso de la función.

      Aunque este código ilustra el orden en el cual se ejecutaría defer, no es una alternativa habitual para usarla cuando se escribe un programa de Go. Es más probable que utilicemos defer para limpiar un recurso, como el controlador de un archivo. Veremos la forma de hacer eso a continuación.

      Utilizar defer para limpiar recursos

      En Go, es muy común usar defer para limpiar recursos. Primero, veremos un programa que escribe una cadena en un archivo, pero no utiliza defer para gestionar la limpieza del recurso:

      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
      }
      

      En este programa, existe una función llamada write que primero intentará crear un archivo. Si tiene un error, lo mostrará y cerrará la función. A continuación, intenta escribir la cadena This is a readme file en el archivo especificado. Si recibe un error, lo mostrará y cerrará la función. A continuación, la función intentará cerrar el archivo y liberar el recurso de vuelta para el sistema. Finalmente, la función muestra nil para indicar que se ejecutó sin errores.

      Aunque este código funciona, hay un error sutil. Si falla la invocación de io.WriteString, la función volverá sin cerrar el archivo ni liberar el recurso de vuelta para el sistema.

      Podríamos solucionar el problema añadiendo otra instrucción file.Close(), método con el cual probablemente resolvería esto en un lenguaje sin 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
      }
      

      Ahora, incluso si la invocación de io.WriteString falla, cerraremos el archivo de todos modos. Aunque este era un error relativamente fácil de detectar y solucionar, con una función más complicada, es posible que se haya pasado por alto.

      En vez de añadir la segunda invocación a file.Close(), podemos usar una instrucción defer para garantizar que independientemente de las secciones que se tomen durante la ejecución, siempre invoquemos Close().

      Aquí está la versión que utiliza la palabra clave 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
      }
      

      Esta vez, añadimos la línea de código: defer file.Close(). Esto indica al compilador que debería ejecutar file.Close antes de cerrar la función write.

      Ahora, nos hemos asegurado de que, incluso si añadimos más código y creamos otra ramificación que cierre la función en el futuro, siempre limpiaremos y cerraremos el archivo.

      Sin embargo, hemos introducido un error más al añadir defer. Ya no comprobaremos el error potencial que puede mostrarse desde el método Close. Esto se debe a que cuando usamos defer no hay forma de comunicar valores de retorno a nuestra función.

      En Go, se considera una práctica segura y aceptada invocar Close() más de una vez sin que esto afecte al comportamiento de su programa. Si Close() muestra un error, lo hará la primera vez que se invoque. Esto nos permite invocarlo explícitamente en la ruta de ejecución correcta de nuestra función.

      Veamos cómo podemos aplicar defer a la invocación para Close y, de todas formas, notificar un error si encontramos uno.

      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()
      }
      

      El único cambio en este programa es la última línea en la que mostramos file.Close(). Si la invocación a Close genera un error, este ahora se mostrará como previsto para la función de invocación. Tenga en cuenta que nuestra instrucción defer file.Close() también se ejecutará después de la instrucción return. Esto significa que file.Close() posiblemente se invoque dos veces. Aunque esto no es lo ideal, es una práctica aceptable porque no debería tener efectos colaterales en su programa.

      Si, sin embargo, vemos un error previamente en la función, como cuando invocamos WriteString; la función mostrará ese error y también intentará invocar a file.Close porque se difirió. Aunque file.Close puede mostrar un error (y probablemente lo haga) también, esto ya no nos importa porque vemos un error que probablemente nos indique el problema.

      Hasta ahora, vimos la forma en que podemos usar un único defer para asegurarnos de limpiar nuestros recursos correctamente. A continuación, veremos la manera en que podemos usar varias instrucciones defer para limpiar más de un recurso.

      Varias instrucciones defer

      Es normal que haya más de una instrucción defer en una función. Crearemos un programa que solo tenga instrucciones defer para ver qué sucede cuando introducimos varias instrucciones defer:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("one")
          defer fmt.Println("two")
          defer fmt.Println("three")
      }
      

      Si ejecutamos el programa, veremos el siguiente resultado:

      Output

      three two one

      Observe que el orden es el opuesto al que empleamos para invocar las instrucciones defer. Esto se debe a que cada instrucción diferida que se invoca se apila sobre la anterior y luego se invoca a la inversa cuando la función sale del ámbito (Last In, First Out).

      Puede tener tantas invocaciones diferidas como sea necesario en una función, pero es importante recordar que todas se invocarán en el orden opuesto en el que se ejecutaron.

      Ahora que comprendemos el orden en el cual se ejecutarán varias instrucciones defer, veremos la forma de usar varias instrucciones defer para limpiar varios recursos. Crearemos un programa que abra un archivo, realice tareas de escritura en él y luego lo abra de nuevo para copiar el contenido a otro archivo.

      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()
      }
      

      Añadimos una nueva función llamada fileCopy. En esta función, primero abrimos nuestro archivo de origen desde el que realizaremos la copia. Comprobaremos si se mostró un error al abrir el archivo. Si es así, aplicaremos return al error y cerraremos la función. De lo contrario, aplicaremos defer al cierre del archivo de origen que acabamos de abrir.

      A continuación, crearemos un archivo de destino. De nuevo, comprobaremos si aparece un error al crear el archivo. Si esto sucede, aplicaremos return a ese error y cerraremos la función. De lo contrario, también aplicaremos defer a Close() para el archivo de destino. Ahora tenemos dos funciones defer que se invocarán cuando la función cierre su ámbito.

      Ahora que ambos archivos están abiertos, aplicaremos Copy() a los datos del archivo de origen al de destino. Si esto se realiza correctamente, intentaremos cerrar ambos archivos. Si observamos un error al intentar cerrar cualquiera de los archivos, aplicaremos return al error y cerraremos el ámbito de la función.

      Observe que invocamos de forma explícita a Close() para cada archivo, aunque defer también invoque a Close(). Esto es para garantizar que notifiquemos el error si hay un error al cerrar un archivo. También garantiza que si, por cualquier motivo, la función se cierra antes de tiempo con un error, por ejemplo, si no pudimos realizar una copia entre los dos archivos, cada uno de ellos intentará cerrarse de forma adecuada a partir de las invocaciones diferidas.

      Conclusión

      En este artículo, incorporó conocimientos sobre la instrucción defer y la forma en que puede usarse para verificar que se hayan limpiado correctamente los recursos del sistema en nuestro programa. Limpiar correctamente los recursos del sistema hará que su programa utilice menos memoria y funcione mejor. Para obtener más información acerca de las aplicaciones de defer, lea el artículo sobre el manejo de Panics o consulte nuestra serie Cómo realizar codifcaciones en Go.



      Source link

      Выражения defer в Go


      Введение

      В Go используется много общих управляющих ключевых слов, которые используются и в других языках программирования. В число этих ключевых слов входят if, switch, for и т. д. Однако некоторые ключевые слова отсутствуют в большинстве языков программирования. Одно из них — ключевое слово defer, и хотя оно используется нечасто, вы быстро поймете, насколько полезно оно может быть для ваших программ.

      Одно из основных назначений выражения defer заключается в очистке ресурсов, в том числе открытых файлов, сетевых подключений и инициализаций в базе данных. Когда ваша программа закончит использовать эти ресурсы, их важно закрыть, чтобы не доводить программу до предела и дать другим программам доступ к этим ресурсам. Выражение defer делает код чище и снижает вероятность ошибок за счет хранения вызовов закрытия файла или ресурса вблизи к вызовам открытия.

      В этой статье мы расскажем о том, как правильно использовать выражение defer для очистки ресурсов, а также о некоторых распространенных ошибках, которые встречаются при использовании defer.

      Что представляет собой выражение defer

      Выражение defer добавляет вызов функции после ключевого слова defer в стеке приложения. Все вызовы в стеке вызываются при возврате функции, в которой они добавлены. Поскольку вызовы помещаются в стек, они производятся в порядке от последнего к первому.

      Посмотрим, как выражение defer работает при выводе текста:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("Bye")
          fmt.Println("Hi")
      }
      

      В функции main два выражения. Первое выражение начинается с ключевого слова defer, за которым идет выражение print, которое выводит текст Bye. Следующая строчка выводит текст Hi.

      Если мы запустим программу, результат будет выглядеть так:

      Output

      Hi Bye

      Обратите внимание, что в первую очередь было выведено слово Hi. Это связано с тем, что любое выражение, перед которым идет ключевое слово defer, не вызывается до конца функции, где использовалось ключевое слово defer.

      Давайте посмотрим на программу еще раз и добавим несколько комментариев, которые помогут проиллюстрировать ситуацию:

      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
      }
      

      Для понимания ключевого слова defer важно знать, что при выполнении выражения defer аргументы отложенной функции оцениваются немедленно. При выполнении defer идущее после него в списке выражение вызывается до возврата функции.

      Хотя этот код иллюстрирует порядок запуска defer, это не совсем обычный способ, который использовался бы при написании программы Go. Более вероятно использование defer для очистки ресурса, например дескриптора файла. Далее мы покажем, как это сделать.

      Использование defer для очистки ресурсов

      Использование defer для очистки ресурсов часто применяется в Go. Вначале рассмотрим программу, которая записывает строку в файл, но не использует 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 {
              return err
          }
          file.Close()
          return nil
      }
      

      В этой программе имеется функция write, которая вначале пытается создать файл. При возникновении ошибки функция выводит сообщение об ошибке и закрывается. Затем она пытается записать строку This is a readme file в указанный файл. При возникновении ошибки функция выводит сообщение об ошибке и закрывается. Затем функция пытается закрыть файл и вернуть ресурс в систему. В заключение функция возвращает значение nil, подтверждая выполнение функции без ошибки.

      Хотя этот код работает, в нем есть небольшая ошибка. Если вызов io.WriteString не обрабатывается надлежащим образом, функция прекращает работу без закрытия файла и возврата ресурса в систему.

      Эту проблему можно решить, добавив еще одно выражение file.Close(), которое позволит решить проблему без использования 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
      }
      

      Теперь программа закроет файл, даже если вызов io.WriteString не будет обработан надлежащим образом. Хотя эту ошибку было относительно легко найти и исправить, в более сложной функции ее можно было и пропустить.

      Вместо добавления второго вызова file.Close() мы можем использовать выражение defer, чтобы вызывать Close() вне зависимости от хода выполнения функции.

      Вот версия, использующая ключевое слово 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
      }
      

      В этот раз мы добавили строчку кода: defer file.Close(). Это указывает компилятору, что функцию file.Close нужно выполнить перед выходом из функции write.

      Теперь программа всегда будет очищать ресурсы и закрывать файл, даже если мы добавим дополнительный код и создадим другое ответвление кода с выходом из функции.

      Однако, добавив defer, мы внесли в код еще одну ошибку. Мы больше не проверяем ошибки, которые может возвратить метод Close. Это связано с тем, что при использовании defer нет способа передать в функцию какое-либо возвращаемое значение.

      В Go считается безопасным и приемлемым вызывать функцию Close() несколько раз, и это не повлияет на поведение программы. Если Close() возвратит ошибку, это произойдет при первом вызове. Это позволит нам явно вызвать эту команду на успешном пути выполнения нашей функции.

      Теперь посмотрим, как мы можем отложить вызов Close и при этом сообщить об ошибке, если она возникнет.

      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()
      }
      

      Единственное изменение этой программы мы внесли в последнюю строку, где мы возвращаем file.Close(). Если при вызове Close возникает ошибка, она будет возвращена вызывающей функции, как и ожидается. Необходимо помнить, что выражение defer file.Close() также будет выполняться после выражения return. Это означает, что функция file.Close() может быть вызвана дважды. Хотя это не идеально, эта практика является допустимой, поскольку она не создаст никаких побочных эффектов для вашей программы.

      Если ошибка возникнет раньше, например при вызове WriteString, функция выведет сообщение об ошибке и попытается вызвать file.Close, поскольку это отложенная команда. Хотя file.Close может также возвратить ошибку, и скорее всего это произойдет, нам больше не нужно беспокоиться о причинах, поскольку сообщение об ошибке укажет на них.

      Мы посмотрели, как использовать одно выражение defer для правильной очистки ресурсов. Далее мы посмотрим, как использовать несколько выражений defer для очистки нескольких ресурсов.

      Использование нескольких выражений defer

      Функция может содержать несколько выражений defer. Создадим программу, которая будет содержать только выражения defer, и посмотрим, что произойдет при использовании нескольких выражений defer:

      main.go

      package main
      
      import "fmt"
      
      func main() {
          defer fmt.Println("one")
          defer fmt.Println("two")
          defer fmt.Println("three")
      }
      

      Если мы запустим программу, результат будет выглядеть следующим образом:

      Output

      three two one

      Обратите внимание, что порядок выполнения противоположен порядку вызова выражений defer. Это связано с тем, что каждое выражение defer помещается в стек поверх предыдущего и вызывается функцией в обратном порядке (Last In, First Out).

      Вы можете использовать в функции любое количество выражений defer, но важно помнить, что они вызываются в порядке, обратном порядку их выполнения.

      Теперь мы понимаем порядок выполнения нескольких выражений defer и можем посмотреть, как использовать несколько выражений defer для очистки нескольких ресурсов. Мы создадим программу, которая открывает файл, выполняет в него запись и снова открывает его для копирования содержимого в другой файл.

      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()
      }
      

      Мы добавили новую функцию с именем fileCopy. В этой функции мы вначале откроем исходный файл, откуда будем производить копирование. Проверим наличие ошибки при открытии файла. Если есть ошибка, мы возвращаем ее и выходим из функции. В противном случае мы используем defer для закрытия исходного файла, который мы только что открыли.

      Затем мы создаем файл назначения. После этого мы снова проверяем наличие ошибки при создании файла. Если она есть, мы возвращаем эту ошибку и выходим из функции. В противном случае мы также используем defer для функции Close() для закрытия файла. Теперь у нас имеется два выражения defer, которые должны вызываться при выходе из функции в ее области действия.

      Теперь у нас открыты оба файла и мы выполним копирование данных с помощью функции Copy() из файла источника в файл назначения. Если копирование будет выполнено успешно, программа попытается закрыть оба файла. Если мы получим сообщение об ошибке при попытке закрытия любого из файлов, мы возвратим эту ошибку и выйдем из области функции.

      Обратите внимание, что мы явно вызываем Close() для каждого файла, хотя defer также вызывает Close(). Это обеспечивает вывод сообщения об ошибке, если при закрытии файла возникнет ошибка. Если по любой причине функция закроется с ошибкой, например, при ошибке копирования между двумя файлами, каждый файл попытается правильно выполнить закрытие из отложенных вызовов.

      Заключение

      В этой статье мы узнали о выражении defer и его использовании для правильной очистки системных ресурсов нашей программы. При правильной очистке ресурсов программа будет потреблять меньше памяти и работать более эффективно. Дополнительную информацию об использовании выражения defer можно найти в статье, посвященной обработке паники, и в других статьях нашей серии по программированию на Go.



      Source link