Tutorial

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

Published on January 24, 2020
Русский
Выражения 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 %s\n", 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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


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!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Featured on Community

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel