Начнем с того, как я открыл для себя силу отсрочки.

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

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

Но defer был полезен не только для файлового ввода-вывода. Вскоре я обнаружил, что могу использовать его для самых разных целей, таких как очистка таких ресурсов, как сетевые подключения, подключения к базам данных и даже ответы HTTP. А поскольку defer работает в обратном порядке, я мог быть уверен, что ресурсы очищались в правильном порядке, независимо от того, насколько сложным стал код.

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

Когда в программе Go встречается оператор defer, среда выполнения Go создаст новый фрейм стека в стеке текущей горутины, называемый «стеком отложенного выполнения». Этот кадр стека содержит информацию об отложенном вызове функции, такую ​​как сама функция и ее аргументы.

Когда отложенный вызов функции добавляется в стек отложенного выполнения, среда выполнения Go записывает текущие значения счетчика программ (PC) и указателя стека (SP), которые указывают на следующую выполняемую команду и вершину текущей кадр стека соответственно.

Позже, когда текущая функция вот-вот вернется, среда выполнения Go сначала раскрутит стек отложенного выполнения в обратном порядке, выполняя вызовы отложенных функций. Для каждого вызова отложенной функции среда выполнения Go будет устанавливать счетчик программ на значение, записанное при добавлении отложенного вызова в стек, фактически «прыгая» назад к точке, где встретился оператор defer. Это позволяет отложенной функции выполняться, как если бы она была частью тела текущей функции, с доступом к ее локальным переменным и параметрам.

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

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

Примечание.Defer следует объявлять перед любыми операторами return. Любой оператор отсрочки, написанный после оператора возврата, не будет выполнен.

Пример кода:

package main

import "fmt"

func main() {
 for i := 1; i <= 5; i++ {
  defer fmt.Print(i)
 }
}

В этом примере мы добавляем операторы defer для печати, что формирует стек отложений, конечным результатом будут числа от 1 до 5 в обратном порядке. Это демонстрирует обратный порядок отсрочки.

Применениеdefer:

1. Очистка ресурсов

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

func readFile(filename string) error {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer f.Close()
    // read from file
    return nil
}

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

2. Регистрация и отслеживание

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

func foo() {
    defer log.Println("foo ended")
    log.Println("foo started")
    // do some work
}

В этом примере оператор log.Println("foo ended") откладывается до конца функции foo, что позволяет вам регистрировать завершение функции. Оператор log.Println("foo started") выполняется немедленно, что позволяет зарегистрировать момент запуска функции.

3. Паническое восстановление

defer также можно использовать для аварийного восстановления, позволяя восстанавливаться после непредвиденных ошибок и корректно их обрабатывать. Например:

package main

import "fmt"

func main() {
    f()
    fmt.Println("Returned normally from f.")
}

func foo() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in f", r)
        }
    }()
    fmt.Println("Calling g.")
    g(0)
    fmt.Println("Returned normally from g.")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic(fmt.Sprintf("%v", i))
    }
    defer fmt.Println("Defer in g", i)
    fmt.Println("Printing in g", i)
    g(i + 1)
}

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

Программа выведет:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
Recovered in f 4
Returned normally from f.

Плюсы и минусы использования отсрочки в Go

Использование defer в Go имеет несколько преимуществ и недостатков, о которых должны знать программисты:

Плюсы использования отсрочки в Go

  1. Простой синтаксис: синтаксис для defer прост и понятен, что упрощает его использование в коде Go.
  2. Очистка ресурсов: defer упрощает надлежащую очистку таких ресурсов, как файлы и сетевые подключения, когда они больше не нужны.
  3. Паническое восстановление: defer можно использовать для панического восстановления, что позволяет восстанавливаться после непредвиденных ошибок и корректно их обрабатывать.
  4. Регистрация и отслеживание: defer можно использовать для регистрации и отслеживания, что позволяет регистрировать начало и конец функции, а также время, необходимое для ее выполнения.

Минусы использования отсрочки в Go

  1. Производительность: defer может повлиять на производительность, особенно если отложенные вызовы функций сложны или многочисленны. Каждый отложенный вызов функции должен быть добавлен в стек, и стек должен быть раскручен, когда функция возвращается.
  2. Удобочитаемость. Чрезмерное использование defer может затруднить чтение и понимание кода, особенно если есть много отложенных вызовов функций, которые выполняются в обратном порядке.
  3. Побочные эффекты: defer могут иметь побочные эффекты, если отложенные вызовы функций изменяют общее состояние или имеют другие непредвиденные последствия. Важно знать о возможных побочных эффектах отложенных вызовов функций при использовании defer в коде Go.

Заключение

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