Понимание отсрочки в го

Вступление

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

Одно из основных применений оператора + defer + - для очистки ресурсов, таких как открытые файлы, сетевые подключения и дескрипторы httd://en.wikipedia.org/wiki/Handle_(computing)[database]. Когда ваша программа завершит работу с этими ресурсами, важно закрыть их, чтобы избежать исчерпания ограничений программы и предоставить другим программам доступ к этим ресурсам. + defer + делает наш код чище и менее подвержен ошибкам, сохраняя вызовы для закрытия файла / ресурса в непосредственной близости от открытого вызова.

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

Что такое + defer +

Оператор + defer + добавляет вызов function после ключевого слова + defer + в стек. Все вызовы в этом стеке вызываются, когда возвращается функция, в которую они были добавлены. Поскольку вызовы размещаются в стеке, они вызываются в порядке «последний пришел - первым вышел».

Давайте посмотрим, как работает + defer +, распечатав некоторый текст:

main.go

package main

import "fmt"

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

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

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

OutputHi
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 +, которая сначала попытается создать файл. Если есть ошибка, она вернет ошибку и выйдет из функции. Затем он пытается записать строку + Это файл readme в указанный файл. Если он получит ошибку, он вернет ошибку и выйдет из функции. Затем функция попытается закрыть файл и выпустить ресурс обратно в систему. Наконец, функция возвращает + 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 {

       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
   }

   _, err = io.WriteString(file, text)
   if err != nil {
       return err
   }
   return nil
}

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

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

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

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

Давайте посмотрим, как мы можем + defer + вызвать + 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 () +. Если вызов + Close + приводит к ошибке, теперь она будет возвращена вызывающей функции, как и ожидалось. Помните, что ваш оператор + defer file.Close () + также будет выполняться после оператора + return. Это означает, что + file.Close () + потенциально вызывается дважды. Хотя это не идеально, это приемлемая практика, поскольку она не должна создавать побочных эффектов для вашей программы.

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

До сих пор мы видели, как мы можем использовать один + 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")
}

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

Outputthree
two
one

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

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

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

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 %s\n", n, source, destination)

   if err := src.Close(); err != nil {
       return err
   }

   return dst.Close()
}

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

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

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

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

Заключение

В этой статье мы узнали об операторе + defer + и о том, как его можно использовать для обеспечения надлежащей очистки системных ресурсов в нашей программе. Правильная очистка системных ресурсов заставит вашу программу использовать меньше памяти и работать лучше. Чтобы узнать больше о том, где используется + defer +, прочитайте статью об обработке паники или изучите весь наш https://www.digitalocean.com/community/tutorial_series/how-to-code-in-go[How To Code в серии Go.