Открывая доступ к ресурсу (файлу, сокету, сетевому соединению), программист думает, как бы потом не забыть его закрыть. И забывает.
Под ресурсом понимаем любой объект, который может быть открыт (получаем доступ) и закрыт (отдаём доступ). Если наш код возьмёт доступ и не отдаст — будет не очень.
Note
Обычно в языках ООП такое решается с помощью контекстных менеджеров (Python) или деструкторов (C++, Java). Однако в Gо это происходит по-другому — через механизм отложенного вызова.
В Go есть оператор, который позволяет запланировать отложенный вызов, — это инструкция defer. Мы рассматривали его кратко в уроке «Особенности языка Go», а сейчас разберём в подробностях.
resource := System.Acquire("resourceID")
defer System.Close(resource) Оператор defer часто применяется на практике, вот только для начинающих не всегда очевидно, как он работает и какие есть подводные камни.
Оператор defer используют внутри функции, а его операндом служит выражение вызова функции. Будем называть эти функции «отложившая» и «отложенная», чтобы избежать путаницы.
Инструкция defer вычисляет аргументы для вызова, но вызов не делает. Вызов выполняется непосредственно перед тем, как отложившая его функция вернёт управление.
func EvaluationOrder(){
defer fmt.Println("deferred")
fmt.Println("evaluated")
} Выведет:
evaluated
deferred
Работу оператора можно описать следующим образом.
- Идёт обычное выполнение программы.
- Наступает очередь выполнения оператора
defer. - Вычисляются операнды отложенной функции, если такие есть.
- Вызов функции вместе со значениями откладывается в специальный стек.
- Выполнение функции продолжается. Если встречается оператор
defer, то повторяем пункты 3 и 4. - Если встречается оператор
return, то функция вычисляет его операнды и сохраняет значение в буфер. - Если стек отложенных вызовов не пустой, то извлекаем из него вызов функции и выполняем его.
- Повторяем пункт 7, пока стек не опустеет.
- Выходим из функции, возвращая значение из буфера.
Важно понимать, что результат функции вычисляется до выполнения отложенных вызовов.
Отложенных вызовов может быть несколько. Тогда они выполняются в обратном порядке, то есть начиная с того, который был отложен последним, так как вызовы складывались в стек.
Отложенная функция может возвращать значения, но они будут проигнорированы и не могут быть использованы.
Также отложенная функция может быть анонимной и заданной литерально. Напомним, что анонимной называется функция, задаваемая литералом по месту использования. Анонимная функция в таком случае задаётся сразу вместе с вызовом.
В таком случае ей могут быть доступны переменные отложившей функции. Произойдёт замыкание (closure). Например, если у отложившей функции есть именованное возвращаемое значение, отложенная функция может его изменить.
func unintuitive() (value string){
defer func() {value = "На самом деле"}() // круглые скобки в конце означают, что функция вызывается
return "Казалось бы"
} Обратите внимание, это работает только с именованными возвращаемыми значениями. Следующий код выведет "Казалось бы":
func intuitive() (string){
value := "Казалось бы"
defer func() {value = "На самом деле"}()
return value
} В чём разница? В первом случае функция возвращает переменную value. При вычислении операнда return ей действительно присваивается значение "Казалось бы", но эта переменная захвачена замыканием и изменяется в нём. После чего она и возвращается из функции.
Во втором случае у нас есть некоторая скрытая переменная ret1, в которую при вызове оператора return копируется значение её операнда. После любые действия с value уже не будут важны.
Также распространённой ошибкой является предположение, что операнды отложенной функции будут вычислены во время её выполнения. Это не так, они вычисляются при выполнении оператора defer:
package main
import "fmt"
func main() {
a := "some text"
defer func(s string) {
fmt.Println(s)
}(a)
a = "another text"
} Программа напечатает "some text".
Оператор defer чаще всего можно увидеть с парными функциями Open()/Close(), Lock()/Unlock(). Его ставят сразу после захвата ресурса, чтобы точно не забыть.
Вот классический пример:
// открываем файл
file, err := os.OpenFile("file.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
// не забываем закрыть файл
defer file.Close()
// работаем с файлом
_, err = file.WriteString("")
if err != nil {
log.Fatal(err)
} Пример применения
Реализуем на основе функции defer замер времени выполнения функции.
Для начала создадим функцию, которая будет измерять время выполнения и выводить его на экран.
func metricTime (start time.Time) {
// функция Now() возвращает текущее время, а функция Sub возвращает разницу между двумя временными метками
fmt.Println(time.Now().Sub(start))
} Теперь применим её внутри какой-нибудь функции.
func VeryLongTimeFunction () {
defer metricTime(time.Now()) // передаём в функцию metricTime значение текущего времени и откладываем её вызов до возврата
// Какие-то долгие вычисления
}defer и panic
Во время выполнения программы могут возникнуть различные непредвиденные обстоятельства, из-за которых дальнейшая работа функции будет невозможна. В таком случае выполнение функции немедленно прекращается, паника передаётся вызывающей функции и затем вверх по стеку, пока выполнение программы не завершится.
Однако этот процесс меняется, если паникующая функция имеет отложенные вызовы. Они будут исполнены после выхода из функции и могут понять, что произошла паника.
Это похоже на механизм исключений в С++ и Python, однако крайне не рекомендуется использовать panic для обычной работы. Вызывать панику следует лишь тогда, когда выполнение программы действительно не может продолжаться и она должна быть завершена.
func PanicingFunc () {
defer func(){
if r := recover(); r != nil { // встроенная функция recover останавливает панику и возвращает описание произошедшего
fmt.Println("Panic is caught", r)
}
}()
///
///
panic("Мне здесь совсем ничего не нравится!")
// встроенная функция panic () вызывает панику у функции.
// в качестве аргумента ей принято передавать причину паники. Именно она будет возвращена функцией recover
}Без применения оператора defer остановить панику было бы невозможно. Он позволяет вклиниться в стек вызовов функций и остановить её. Заметьте, что не всякую панику можно восстановить. Иногда возникают особые ситуации, когда recover не срабатывает.
📂 Go | Последнее изменение: 22.08.2024 13:38