Открывая доступ к ресурсу (файлу, сокету, сетевому соединению), программист думает, как бы потом не забыть его закрыть. И забывает.

Под ресурсом понимаем любой объект, который может быть открыт (получаем доступ) и закрыт (отдаём доступ). Если наш код возьмёт доступ и не отдаст — будет не очень.

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 

Работу оператора можно описать следующим образом.

  1. Идёт обычное выполнение программы.
  2. Наступает очередь выполнения оператора defer.
  3. Вычисляются операнды отложенной функции, если такие есть.
  4. Вызов функции вместе со значениями откладывается в специальный стек.
  5. Выполнение функции продолжается. Если встречается оператор defer, то повторяем пункты 3 и 4.
  6. Если встречается оператор return, то функция вычисляет его операнды и сохраняет значение в буфер.
  7. Если стек отложенных вызовов не пустой, то извлекаем из него вызов функции и выполняем его.
  8. Повторяем пункт 7, пока стек не опустеет.
  9. Выходим из функции, возвращая значение из буфера.

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

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

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

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

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