Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.
Хой, джедаи и амазонки!
Нашёл интересное применение defer помимо использования для file.Close(), и хочу им поделиться. А также хочу рассказать о другой полезной информацией, которую не найдёшь в общедоступных материалах.
1. Базовые знания
Первое, что нужно знать о 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 я встречал три термина:
- Ключевое слово;
- Выражение;
- Оператор.
На мой взгляд верен только первый пункт. Можно как синоним использовать и второй. А вот слово оператор ,более логично, означает совсем другое.
Вот что о сравнении этих двух терминов думает нейросеть:
Далее зададим нейросети отдельные вопросы по каждому термину и сравним результаты.
2.1. Операторы
Вот что думает об операторах нейросеть:
Что хочу сказать по поводу ответа нейросети:
По пункту 8. Оператор типов type - это уже скорее к ключевому слову относится... А typeof вообще не понял, что имелось ввиду.
По пункту 9. Операторы диапазонов обозначаются ключевым словом range. А здесь по-видимому имеется ввиду синтаксис сразу после for:
for idx, el := range arr {
// код
}
2.2. Ключевые слова
Вот что нейросеть думает по части ключевых слов:
Просто как наблюдение. Пока не попросил нумерованный список, ответ нейросети был такой:
Ну и ещё - попросил нейросеть составить этот же перечень, но в алфавитном порядке. В этот раз нейросеть написала более подробный перевод с пояснениями:
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
Допустим, у нас есть задача вывести в терминал элементы массива в обратном порядке. Простая задача, которую можно реализовать кодом:
А можно через defer:
Обратите внимание, при инициализации цикла я использовал другое описание.
Вопрос - для чего нам два способа? Ответ - пока что просто для понимания, что такая возможность есть.
В 3D моделировании, которым я занимался на работе 10 лет, любое действие инженер может выполнить примерно пятью разными действиями, не меньше. Но исходя из практики на уровне подсознания автоматом выполняю тем способом - который наиболее простой.
Вот и тут - знакомимся с инструментарием и раскладываем его по-полочкам.
А вот интересная штука - если в defer поместить весь цикл, а не его содержание, код будет выполняться в прямом, а не обратном порядке:
Почему так? На первый взгляд - не логично.
На второй взгляд - благодаря циклу, описанному в Рис. 3.2.2, мы по сути имеем пять последовательных defer. А мы помним, что если у нас несколько defer, то вызываются в обратном порядке, см. раздел 3.1.
Внутри одной defer функции вызываются в прямом порядке:
Соответственно, если мы помещаем весь цикл в defer, то мы вызываем defer один раз. И порядок действий функций, которые обёрнуты в один defer, будет в прямом порядке. Всё логично.
3.3. Вызов defer вне main()
Смоделируем ситуацию - мы выполняем некие вычисления, обёрнутые в defer внутри неосновной функции. Напомню - основная функция программы - main(), она является точкой входа в программу.
Вопрос - как добиться возвращения вычислений, обёрнутых в defer из одной функции в другую? Пример решения ниже:
Что здесь интересного. В описании сигнатуры функции в строке 11 мы сразу ввели возвращаемую переменную. Соответственно, в теле функции мы повторно переменную result не объявляем и не прописываем после ключевого слова return.
Ниже на схеме представлен процесс работы 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) - это критическая ошибка в программе, которая приводит к аварийному завершению ее работы. Когда программа не может обработать ошибку и не знает, что делать дальше, она вызывает панику.
Во время выполнения программы возникают ошибки, которые можно разделить на два типа:
- Ожидаемые программистом (например, ошибки ввода-вывода, ошибки сетевого подключения, ошибки типов данных и ошибки доступа к файлам или директориям);
- Неожидаемые программистом (непредвиденные).
Паники относятся ко второму типу. Вызовем панику принудительно:
Мы видим, что функция печати (просто цифра 3) выполнена, не смотря на появление паники в строке 9.
Или другой пример, где сам компилятор паникует без принудительной паники:
Здесь мы обращаемся к элементу среза вне его диапазона. Компилятор паникует.
Паника появляется, если:
- При выходе за границы массива или среза;
- Неправильное использование указателей;
- Разделить число на ноль;
- При вызове методов на нулевых значениях;
- Некорректное использование горутин и каналов.
Последние два пункта пока для меня тёмный лес. Постепенно освоим и это.
Структуру сообщений и обработку паники я разбирать здесь не буду. Это тема отдельной серии публикаций. Так, например, на платформе Stepik есть объёмный платный курс по обработке ошибок (наверняка, там есть и про паники):
Сейчас просто важно знать, что если окружающая функция паникует, всё что обёрнуто в defer будет выполняться. А выполняться там может то, что обработает панику и предотвратит аварийное завершение кода.
5. Выводы
Лучше разобрались с ключевым словом defer. Повысили нашу инженерную насмотренность - разобрали, что лучше называть операторами, что ключевыми словами. Посмотрели как часто в программах используют те или иные ключевые слова и операторы.
Посмотрели примеры кода. Главное, что я хотел донести этой публикацией заключено в нескольких тезах:
- Если в функции несколько ключевых слов defer, то они выполняются в обратном порядке - раньше выполнится тот defer, что ближе к концу функции;
- Функции os.Exit() или log.Fatal() завершают функцию без вызова defer;
- Операции, обёрнутые в defer вызываются в прямом порядке, а не обратном;
- Можно из внешней функции вернуть результат выражения defer, используя особый синтаксис;
- Используя defer, можно обрабатывать непредвиденные ошибки.
В принципе, у меня всё. Надеюсь, публикация была полезной.
--//--//--
PS Если захочешь купить курс от SkillBox, воспользуйся моей реферальной ссылкой. Ты получишь огромную скидку на курс и плюс в карму за помощь каналу.
Бро, ты уже здесь? 👉 Подпишись на канал для новичков «Войти в IT» в Telegram, будем изучать IT вместе 👨💻👩💻👨💻