Как работает defer в Go

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

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

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

Оператор defer

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

Читайте также: Определение и вызов функций в Go

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

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

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

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

Hi
Bye

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

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

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

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

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

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

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

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

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

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

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

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

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

Давайте посмотрим, как использовать в программе defer и Close и по-прежнему возвращать ошибку, если она была обнаружена.

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, и посмотрим, что происходит в подобных ситуациях:

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

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

three
two
one

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

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

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

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. В этой функции сначала открывается исходный файл, из которого нужно скопировать строку. Программа проверит, была ли ошибка при открытии файла. Если это так, она возвращает эту ошибку и выходит из функции. В противном случае программа откладывает закрытие исходного файла, который только что открыла.

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

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

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

Tags: ,