Добавить в корзинуПозвонить
Найти в Дзене
Я, Golang-инженер

#46. defer: способы применения

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением. Хой, джедаи и амазонки! Нашёл интересное применение defer помимо использования для file.Close(), и хочу им поделиться. А также хочу рассказать о другой полезной информацией, которую не найдёшь в общедоступных материалах. 1. Базовые знания Первое, что нужно знать о defer - это ключевое слово, а не пакетная функция. Соответственно, информацию о ней нужно искать не в официальной документации, а в спецификации. Базовое применение defer в Go - выполняется для гарантированного закрытия файла при создании/открытии файла. Это нужно, чтобы память устройства освобождалась от информации, которая уже не используется. Рассмотрим код: Эта программа создаёт файл в строке 10, обрабатывает вероятную ошибку создания файла, а в строке 15 вызывает отложенное выполнении функции file.Close() для закрытия файла за счёт ключевого слова defer. Файл в данном случае будет
Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой, джедаи и амазонки!

Нашёл интересное применение defer помимо использования для file.Close(), и хочу им поделиться. А также хочу рассказать о другой полезной информацией, которую не найдёшь в общедоступных материалах.

1. Базовые знания

Первое, что нужно знать о defer - это ключевое слово, а не пакетная функция. Соответственно, информацию о ней нужно искать не в официальной документации, а в спецификации.

Фрагмент описания defer из специцикации
Фрагмент описания defer из специцикации

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

Рассмотрим код:

Код
Код

Эта программа создаёт файл в строке 10, обрабатывает вероятную ошибку создания файла, а в строке 15 вызывает отложенное выполнении функции file.Close() для закрытия файла за счёт ключевого слова defer.

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

В каких случаях функции, обёрнутые в defer, будут вызываться, а в каких - нет?

Согласно спецификации Go, defer вызывается в трёх случаях:

  • Закончено выполнение функции, в которой вызывается defer (окружающая функция);
  • Окружающая функция выполнила оператор return (например, в теле цикла);
  • Программа паникует (см. раздел 4 публикации).

В каких случаях defer не вызывается? Я знаю о трёх таких случаях:

  • При обработке ошибок, или в любом другом случае, когда мы вызываем os.Exit();
  • Вызываем log.Fatal(), т.к. внутри неё также прячется os.Exit();
  • Паника возникает до вызова defer.

Это ключевая на мой взгляд информация о defer. А сейчас сделаю отступление в целях роста инженерной насмотренности, в котором рассмотрим, что такое ключевое слово и чем оно отличаются от оператора.

2. Ключевое слово или оператор?

В первой иллюстрации был англоязычный фрагмент из спецификации Go. Там в отношении defer использовалось слово statement. С английского это слово переводится как оператор.

В русскоязычном сообществе в отношении defer я встречал три термина:

  1. Ключевое слово;
  2. Выражение;
  3. Оператор.

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

Вот что о сравнении этих двух терминов думает нейросеть:

Мнение нейросети "оператор vs ключевое слово"
Мнение нейросети "оператор vs ключевое слово"

Далее зададим нейросети отдельные вопросы по каждому термину и сравним результаты.

2.1. Операторы

Вот что думает об операторах нейросеть:

Перечень операторов
Перечень операторов

Что хочу сказать по поводу ответа нейросети:

По пункту 8. Оператор типов type - это уже скорее к ключевому слову относится... А typeof вообще не понял, что имелось ввиду.

По пункту 9. Операторы диапазонов обозначаются ключевым словом range. А здесь по-видимому имеется ввиду синтаксис сразу после for:

for idx, el := range arr {
// код
}

2.2. Ключевые слова

Вот что нейросеть думает по части ключевых слов:

Перечень ключевых слов в Go
Перечень ключевых слов в Go

Просто как наблюдение. Пока не попросил нумерованный список, ответ нейросети был такой:

Перечень ключевых слов в Go
Перечень ключевых слов в Go

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

1. break - прервать выполнение цикла или switch;
2.
case - определить ветвь в switch;
3.
chan - канал для обмена сообщениями между горутинами;
4.
const - объявление константы;
5.
continue - перейти к следующей итерации цикла;
6.
default - ветвь по умолчанию в switch;
7.
defer - отложенный вызов функции до завершения текущей функции;
8.
else - ветвь условного оператора, выполняемая при ложном условии;
9.
fallthrough - продолжить выполнение следующей ветви в switch;
10.
for - цикл с предусловием или постусловием;
11.
func - объявление функции;
12.
go - запустить новую горутину;
13.
goto - перейти к метке в текущей функции;
14.
if - условный оператор;
15.
import - импортировать пакет;
16.
interface - определение интерфейса;
17.
map - отображение (ассоциативный массив);
18.
package - объявление пакета;
19.
range - итерация по элементам массива, среза или отображения;
20.
return - вернуть значение из функции;
21.
select - выбрать готовый к чтению или записи канал;
22.
struct - определение структуры данных;
23.
switch - множественный условный оператор;
24.
type - определение нового типа;
25.
var - объявление переменной.

И ещё любопытная информация:

Частота применения ключевых слов
Частота применения ключевых слов

Неточность в том, что сумма не равна 100%. Что ж, до Скайнет ещё далеко, и это радует.

Попробуем получить статистику по операторам:

Ответ нейросети
Ответ нейросети

Как видим, в строки 5, 8 и 9 нейросеть запихнула ключевые слова. Но для общего понимания - на мой взгляд интересная информация.

2.3. Выводы по операторам и ключевым словам

Для себя решил, чтобы не путаться - операторами называть знаки, ключевыми словами - текстовые "выражения".

А так, есть пословица: "Хоть горшком назови, только в печь не сажай". В том плане, не столь важно, как ты называешь объект, сколько, какой смысл туда вкладываешь.

И напоминаю, переменную нельзя назвать именем ключевого слова, или использовать в имени переменной знаки операторов. При компиляции будет синтаксическая ошибка "unexpected..."

3. Что ещё полезно знать о defer

Разберём несколько примеров использования defer. В целом, коллеги говорят, что у defer применений очень много: от реализации стека, до ловли panic'ов через recover. Это на мой взгляд уже продвинутый уровень. Мне важно разобраться в базовых механиках defer.

3.1. Порядок вызова нескольких defer

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

Фрагмент кода
Фрагмент кода

Пояснения, думаю не требуются. Просто усвоили - что defer так работает. Аналогичным образом устроена работа внутри каждого defer - см. следующий пункт.

Чем может быть полезно такое свойство defer? Коллеги говорят, что это полезно, когда нужно обработать значения в порядке, обратном тому, в котором они "прилетели".

3.2. Цикл в defer

Допустим, у нас есть задача вывести в терминал элементы массива в обратном порядке. Простая задача, которую можно реализовать кодом:

Рис. 3.2.1. Вывод массива в обратном порядке
Рис. 3.2.1. Вывод массива в обратном порядке

А можно через defer:

Рис. 3.2.2. Вывод массива в обратном порядке
Рис. 3.2.2. Вывод массива в обратном порядке

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

Вопрос - для чего нам два способа? Ответ - пока что просто для понимания, что такая возможность есть.

В 3D моделировании, которым я занимался на работе 10 лет, любое действие инженер может выполнить примерно пятью разными действиями, не меньше. Но исходя из практики на уровне подсознания автоматом выполняю тем способом - который наиболее простой.

Вот и тут - знакомимся с инструментарием и раскладываем его по-полочкам.

А вот интересная штука - если в defer поместить весь цикл, а не его содержание, код будет выполняться в прямом, а не обратном порядке:

Рис. 3.2.3. Прямой вывод значений массива с defer
Рис. 3.2.3. Прямой вывод значений массива с defer

Почему так? На первый взгляд - не логично.

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

Внутри одной defer функции вызываются в прямом порядке:
Рис. 3.2.4. Вызов нескольких функций внутри defer
Рис. 3.2.4. Вызов нескольких функций внутри defer

Соответственно, если мы помещаем весь цикл в defer, то мы вызываем defer один раз. И порядок действий функций, которые обёрнуты в один defer, будет в прямом порядке. Всё логично.

3.3. Вызов defer вне main()

Смоделируем ситуацию - мы выполняем некие вычисления, обёрнутые в defer внутри неосновной функции. Напомню - основная функция программы - main(), она является точкой входа в программу.

Рис. 3.3.1. Код
Рис. 3.3.1. Код

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

Рис. 3.3.2. Код
Рис. 3.3.2. Код

Что здесь интересного. В описании сигнатуры функции в строке 11 мы сразу ввели возвращаемую переменную. Соответственно, в теле функции мы повторно переменную result не объявляем и не прописываем после ключевого слова return.

Ниже на схеме представлен процесс работы defer в функции:

Схема работы defer
Схема работы defer

К слову, такую схему я за пять минут начертил здесь <<<

  • Пункт 1 из схемы выше выполняется в строке 11 кода на Рис. 3.3.2;
  • Пункт 2 схемы в строке 12 кода на Рис. 3.3.2;
  • А на третьем этапе схемы происходит интересное. Функция, обёрнутая в defer меняет возвращаемое значение из окружающей функции (для "интовых" переменных по-умолчанию равен нулю).

Такой принцип работы. Почему подобной вещи не происходит в коде на Рис. Рис. 3.3.1? Такой синтаксис. На мой взгляд это что-то вроде синтаксического сахара, позволяющего проворачивать такую штуку. В любом случае полезно знать, что из внешней функции можно вернуть результат вычислений, обёрнутых в defer.

3.4. Гарантированное закрытие файла во внешней функции

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

В предыдущем посте я рассказывал о типе данных *os.File и давал ссылку на GitHub с примером кода. Ниже упрощённый код с гитхаба, демонстрирующий синтаксис закрытия файла во внешней функции.

Код
Код

Что здесь интересного - функция мэйн состоит из пяти строк. Есть две внешние функции:

  • Одна создаёт файл;
  • Вторая закрывает файл.

Закрытие файлов во внешней функции может быть полезно, когда предстоит работа со множеством файлов.

4. Паника

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

Паника в Golang (panic) - это критическая ошибка в программе, которая приводит к аварийному завершению ее работы. Когда программа не может обработать ошибку и не знает, что делать дальше, она вызывает панику.

Во время выполнения программы возникают ошибки, которые можно разделить на два типа:

  1. Ожидаемые программистом (например, ошибки ввода-вывода, ошибки сетевого подключения, ошибки типов данных и ошибки доступа к файлам или директориям);
  2. Неожидаемые программистом (непредвиденные).

Паники относятся ко второму типу. Вызовем панику принудительно:

Код
Код

Мы видим, что функция печати (просто цифра 3) выполнена, не смотря на появление паники в строке 9.

Или другой пример, где сам компилятор паникует без принудительной паники:

Код
Код

Здесь мы обращаемся к элементу среза вне его диапазона. Компилятор паникует.

Паника появляется, если:

  • При выходе за границы массива или среза;
  • Неправильное использование указателей;
  • Разделить число на ноль;
  • При вызове методов на нулевых значениях;
  • Некорректное использование горутин и каналов.

Последние два пункта пока для меня тёмный лес. Постепенно освоим и это.

Структуру сообщений и обработку паники я разбирать здесь не буду. Это тема отдельной серии публикаций. Так, например, на платформе Stepik есть объёмный платный курс по обработке ошибок (наверняка, там есть и про паники):

Курс по обработке ошибок
Курс по обработке ошибок

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

5. Выводы

Лучше разобрались с ключевым словом defer. Повысили нашу инженерную насмотренность - разобрали, что лучше называть операторами, что ключевыми словами. Посмотрели как часто в программах используют те или иные ключевые слова и операторы.

Посмотрели примеры кода. Главное, что я хотел донести этой публикацией заключено в нескольких тезах:

  1. Если в функции несколько ключевых слов defer, то они выполняются в обратном порядке - раньше выполнится тот defer, что ближе к концу функции;
  2. Функции os.Exit() или log.Fatal() завершают функцию без вызова defer;
  3. Операции, обёрнутые в defer вызываются в прямом порядке, а не обратном;
  4. Можно из внешней функции вернуть результат выражения defer, используя особый синтаксис;
  5. Используя defer, можно обрабатывать непредвиденные ошибки.

В принципе, у меня всё. Надеюсь, публикация была полезной.

--//--//--

PS Если захочешь купить курс от SkillBox, воспользуйся моей реферальной ссылкой. Ты получишь огромную скидку на курс и плюс в карму за помощь каналу.

John Lee john_artifexfilm https://unsplash.com/photos/oMneOBYhJxY
John Lee john_artifexfilm https://unsplash.com/photos/oMneOBYhJxY

Бро, ты уже здесь? 👉 Подпишись на канал для новичков «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻