Как работает функция Defer в Golang


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

Для тех, кто только знакомится с Golang, возможно, будет полезно почитать сначала эти статьи:

  • Конкурентность и параллелизм в Golang. Горутины.
  • Обработка ошибок в Golang с помощью Panic, Defer и Recover
  • Введение в каналы Golang

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

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

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

    Для выполнения нескольких задач можно было бы воспользоваться сложными функциями с множеством ключевых слов return внутри каждой из них. При возврате из функции может потребоваться выполнить кое-какие задачи по очистке.

    К числу таких задач могут относиться:

  • Закрытие подключения к файлу.
  • Закрытие подключения к базе данных.
  • Отмена подписки на события.
  • Очистка ресурса в ситуациях, связанных с появлением ошибки.
  • Каждый разработчик будет стремиться к тому, чтобы такие очистки проводились до завершения функции. При этом могут возникать неожиданные ситуации, когда функция возвращает значение в обход обычного маршрута завершения. Во всех этих ситуациях нужен механизм, обеспечивающий выполнение очистки. Работая в сложных приложениях реального времени, разработчик может упускать места и сценарии, где требуется код, связанный с очисткой. Давайте обратимся к конкретному примеру, чтобы разобраться во всём этом.

    Ссылка здесь.

    Здесь мы пытаемся получить доступ к содержимому файла. После его прочтения нужно закрыть подключение к файлу. Посмотрите на большую функцию, выполняющую несколько действий над поступающими данными. В соответствии с данными от файловой системы, она определяет внутри функции множественный «возврат». Прежде чем выходить из функции, нужно убедиться, что файл закрыт. Так что file.Close() пришлось бы вставлять сразу в несколько частей кода. Посмотрите на следующий код, чтобы было понятнее.

    func GetFileContent() string { file, _ := os.Open("./FileContent/sampleFile.txt") data := make([]byte, 100) file.Read(data) dataString := string(data) fmt.Printf(dataString) if(dataString == "Mayank") { file.Close() return "Mayank"; } else { file.Close() return "Anshul" } }

    Внутри этой функции у нас множественный возврат. А это значит, что файл придётся закрывать в нескольких местах и сложность кода будет только увеличиваться. Разработчику надо быть внимательным: из-за усложнения кода он может упустить ту его часть, где требуется освобождение ресурсов.

    Работа с функцией defer

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

    func InvokeDeferFunction() { defer DeferFunctionCall() fmt.Println("Defer Function Still Not Called...") } func DeferFunctionCall() { fmt.Println("Defer Function is called after function call...") }

    Здесь важны два пункта:

  • Ключевое слово defer добавляется перед DeferFunctionCall().
  • Функция defer добавляется в начало выполнения функции.
  • Вывод такой:

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

    Добавление нескольких функций defer

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

    package main import "fmt" func main() { InvokeDeferFunction() } func InvokeDeferFunction() { defer DeferFunctionCall() defer OtherDeferFunctionCall() fmt.Println("Still executing InvokeDeferFunction") } func DeferFunctionCall() { fmt.Println("Defer Function Called...") } func OtherDeferFunctionCall() { fmt.Println("Other Defer Function called...") }

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

    Можно отметить два обстоятельства:

  • Вызываются несколько функций defer.
  • Функция defer, добавленная в конец, будет вызвана первой.
  • Поэкспериментировать предлагаю в этом виртуальном редакторе:

    Ссылка здесь

    Работа с файловыми ресурсами

    Вернёмся к нашему первому примеру с чтением файла. Теперь мы можем переписать код, используя функцию defer. Посмотрите, как выглядит код в этом случае:

    func GetFileContent() string { file, _ := os.Open("./FileContent/sampleFile.txt") defer file.Close() data := make([]byte, 100) file.Read(data) dataString := string(data) fmt.Printf(dataString) if(dataString == "Hello Mayank") { return "Mayank"; } else { return "Anshul" } }

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

    При этом код будет лёгким, так как:

  • Функцию для удаления/закрытия ресурсов можно поставить на то же место, где она находилась. При открытии файла или подключении к базе данных можно добавить в следующей строке внутреннюю функцию для закрытия ресурсов: так разработчик не забудет об этом, даже если конструкция функции очень сложная.
  • У каждой функции defer могут быть свои задачи в коде. Одна функция defer может заниматься извлечением файловых ресурсов, другая — закрытием сетевого подключения. Так что у нас здесь что-то вроде разделения труда.
  • Обработка ошибок с помощью defer

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

  • Panic
  • Defer
  • Recover
  • Интересно, как с помощью ключевого слова defer происходит обработка ошибок в Golang? Подробно ознакомиться с этим можно в следующей статье:

    Обработка ошибок в Golang с помощью Panic, Defer и Recover

    Заключение

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


    Перевод статьи


    Поделиться статьей:


    Вернуться к статьям

    Комментарии

      Ничего не найдено.