Структура ошибок в Go
В отличие от популярных языков, таких как JavaScript, Python, С++, в Go принято взаимодействовать с ошибками через отдельное возвращаемое значение типа error. При этом нулевое значение (nil) говорит о том, что ошибки не возникло. Такой подход позволяет наглядно выделить функции, возвращающие ошибки, и обрабатывать их с использованием обычных синтаксических конструкций.
Тип error
представляет собой интерфейс с единственным методом Error
, возвращающим текстовое описание ошибки:
Идиоматическим способом обработки ошибок является проверка возвращаемого значения с помощью условного оператора if
. Проиллюстрируем это на примере функции strconv.Atoi
из пакета strconv со следующей сигнатурой: func Atoi(s string) (int, error)
Можем видеть, что она возвращает два значения – результат преобразования строки в число и ошибку:
Так как мы передали строку, состоящую не только из цифр, то в результате выполнения кода значение err будет отлично от nil, и в консоль будет выведено описание возникшей ошибки: strconv.Atoi: parsing "123a": invalid syntax
.
Создание ошибок
Для создания ошибок с произвольным текстом используются функции errors.New() и fmt.Errorf():
Часто возникает необходимость создания индивидуальных типов ошибок для обработки конкретных сбоев в программе. Для этого нужно реализовать у этих типов интерфейс error посредством создания метода Error()
:
В некоторых случаях может потребоваться добавить дополнительные данные в описание ошибки, такие как номера строк, названия пакетов, адреса портов и так далее. Для этих целей стоит использовать структуры, реализующие метод Error()
интерфейса error.
В качестве иллюстрации этого метода обработки можно привести тип ParseError из пакета net стандартной библиотеки:
Интерфейс error требует реализации единственного метода Error()
, но для отдельных случаев бывает полезно определить дополнительные. К примеру, в том же пакете net стандартной библиотеки некоторые реализации ошибок имеют вспомогательные методы, определенные интерфейсом net.Error
:
В подобных ситуациях для обработки ошибки можно использовать механизм type assertion
, который позволит определить её тип. Проиллюстрируем это на конкретном блоке кода, который проверяет постоянной или временной является возникшая ошибка подключения. В первом случае установим задержку, после которой продолжим выполнение, а во втором – выведем ошибку и завершим программу:
Правила именования ошибок
Существует несколько соглашений по именованию ошибок, принятых разработчиками и сообществом Go. Следование им поможет улучшить читаемость кода и обеспечить его единообразие.
Базовое правило именования ошибок заключается в том, что error-переменные начинаются с err или Err, а error-типы заканчиваются на Error
:
Также стоит запомнить, что строка описания ошибки не должна начинаться с большой буквы.
В описании ошибок следует указывать их примерное местоположение: название пакета, функции или метода. К примеру, в пакете expression текстовое описание ошибки валидации математического выражения может выглядеть так: “expression: invalid format”.
Panic
Panic (паника) – это встроенная функция, которая останавливает поток выполнения программы и запускает механизм паники. Он похож на исключения в C++ и Java, и может быть вызван ошибками runtime
, такими как выход за границы массива, деление на ноль, а также напрямую с помощью ключевого слова panic
.
Рассмотрим механизм паники на конкретном примере: создадим функцию DivideNums
для деления вещественных чисел. Если делитель равен нулю, то вызовем panic
с текстом "Division by zero"
, иначе – вернем результат деления:
В случае вызова функции с аргументами 1 и 0 возникнет паника, на экран будет выведено примерно следующее:
Разберем процесс паники подробнее. Когда некоторая функция func
вызывает panic
, в программе происходит следующее:
- Останавливается выполнение
func
. - Вызываются все её внутренние defer-функции.
- Запускаются defer-функции, связанные с
func
вплоть до верхнего уровня в исполняемой горутине. - Программа заканчивает выполнение и выводит ошибку, включая значение аргумента panic.
Паника тесно связана с важной сущностью Go – горутинами. Они представляют собой независимые функции, выполняющиеся конкурентно в одном и том же адресном пространстве, являются аналогом корутин в других языках. Горутины имеют динамический стек и управляются рантаймом Go. Главной горутиной является функция main, её завершение приводит к окончанию работы всей программы.
Более подробно горутины будут рассмотрены в последующих частях, в контексте текущей статьи достаточно лишь общего понимания. Для полноценного погружения в тему рекомендуем прочитать материал «Горутины: что такое и как работают».
Recovery
В условиях промышленной разработки часто возникает необходимость обработать панику и вернуть приложение к нормальному выполнению, предотвратив его внезапное завершение. Для этих целей используется механизм восстановления (recovery), реализующийся при помощи встроенной функции recover. Она позволяет восстановить контроль над паникующей горутиной и может быть вызвана только внутри defer-функций.
Recover используется в тех случаях, когда паника не должна привести к завершению всей программы. Например, ошибка в одном из клиентских подключений веб-сервера не должна привести к сбою всего серверного приложения. С помощью recover также можно обрабатывать ошибки в стеке рекурсивных функций и логировать возникшие в программе паники.
Функция recover возвращает nil
, когда горутина не паникует или recover не был напрямую вызван в defer-функции. В иных случаях возвращается значение, отличное от nil
. Если какая-либо горутина запаниковала, то вызов recovery досрочно остановит раскручивание стека, вернет аргумент, переданный в panic
, и возобновит дальнейшее выполнение программы.
При работе с recover следует помнить два важных правила:
recover()
используется только внутри defer-функцийrecover()
работает только в той горутине, где была вызвана паника
Рассмотрим применение recover
на предыдущем примере с функцией DivideNums
. На этот раз запустим механизм восстановления после возникновения паники “Division by zero”:
После выполнения кода получим следующий вывод:
Давайте детально разберем работу написанной программы. При передаче аргументов (10, 0) в функцию DivideNums возникает паника, которая запускает отложенный вызов defer. Он проверяет возникновение паники с помощью recover()
: если значение не равно nil
, то паника обрабатывается, на экран выводится сообщение “Recovery: Division by zero”, после чего программа возвращается к нормальному выполнению. Поскольку произошла паника, функция DivideNums()
завершается, возвращая значение по умолчанию для float32
, равное 0. Далее следует вызов DivideNums
с двумя ненулевыми числами, поэтому в результате возвращается ожидаемое значение (2), которое выводится на экран.
Логирование
В продолжение темы обработки ошибок изучим эффективный инструмент для их своевременного отслеживания – логирование.
Логирование – это процесс записи информации обо всех событиях, происходящих в программе. Как правило, полученные данные записываются в специальные файлы, называемые логами. Они содержат сообщения об ошибках, предупреждения, текущее состояние программы, а также пользовательские действия и другую важную информацию.
Логирование полезно по многим причинам:
- Отладка и диагностика ошибок. Логи позволяют разработчикам отслеживать и анализировать ошибки, возникающие в процессе выполнения программы. Записанные данные могут помочь в решении проблем и выявлении их причин.
- Мониторинг и анализ производительности. В логах может содержаться информация о времени выполнения различных операций и объеме потребляемых ресурсов. Это позволяет отслеживать производительность приложения и выявлять места, требующие оптимизации.
- Аналитика и метрики. Логи могут использоваться для сбора данных о поведении пользователей для анализа их активности, предпочтений. Такая информация нужна для улучшения пользовательского опыта и оптимизации функциональности.
- Информационная безопасность. Логирование играет важную роль в обеспечении безопасности приложений, позволяя отслеживать несанкционированный доступ, утечки данных и другие потенциальные угрозы.
Далее рассмотрим различные пакеты для логирования в Go: log, logrus и slog.
Стандартный пакет log
Разработчики языка предусмотрели стандартный пакет log для простого логирования информации. Он ограничен в функциональности, так как предоставляет лишь базовый набор инструментов для частых задач.
Рассмотрим основные функции пакета log:
- Print, Println, Printf – работают как аналогичные функции из пакета fmt.
- Panic, Panicf, Panicln – работают как функции
Print
, но после вывода текста вызываютpanic()
. - Fatal, Fatalln, Fatalf – работают как функции Print, но после вывода текста завершают программу путем вызова
os.Exit(1)
. - SetFlags – устанавливает флаги для форматирования. К примеру, флаг
Lmicroseconds
добавит микросекунды ко времени,Lshortfile
укажет короткий путь к файлу, откуда было получено сообщение. - SetOutput – указывает направление вывода логов. Можно указать любой объект, реализующий интерфейс
io.Writer
: файл, буфер, сетевое соединение и так далее. - SetPrefix – устанавливает префикс для логов.
Продемонстрируем применение этих функций в коде:
В результате выполнения кода получим следующий результат, который будет различаться в зависимости от времени запуска:
Пакет logrus
Пакет logrus расширяет функции пакета log, предоставляя следующие полезные возможности: задание уровней логирования, настройка формата вывода, внесение в сообщения дополнительной информации произвольного типа и другие.
Logrus предоставляет 7 уровней логирования, упорядоченных по возрастанию значимости в программе: Trace, Debug, Info, Warning, Error, Fatal and Panic. Уровень задается с помощью функции SetLevel. Ниже представлен код, демонстрирующий применение всех уровней логирования и их назначение:
В результате будут выведены цветные префиксы уровней логирования с их описанием:
Отметим, что если указать logrus.SetLevel(logrus.InfoLevel)
вместо logrus.SetLevel(logrus.TraceLevel)
, то сообщения уровней ниже Info, то есть Trace и Debug, выведены не будут.
Logrus позволяет добавить к данным дополнительное описание с помощью функции logrus.WithFields
:
Приведенный выше код выведет сообщение о старте сервера с указанием двух дополнительных полей: адреса (:8080) и протокола (tcp):
Для установки формата вывода логов используется функция logrus.SetFormatter
В результате выполнения кода на первой строке получим сообщение в JSON формате, а на втором – в текстовом с заданными параметрами:
Пакет slog
Пакет slog был предложен сообществом энтузиастов как альтернатива log, после чего поддержан разработчиками и выпущен в версии Go v1.21 по адресу log/slog. Он обладает обратной совместимостью с log и заимствует из него некоторые функции, но обладает расширенным функционалом и возможностью детальной настройки.
В пакете slog присутствует 4 основных уровня логирования, которые идентифицируются целыми числами с интервалом в 4: Debug (-4), Info (0), Warn (4), Error (8). Такой подход предоставляет пользователям возможность добавить свои уровни между четырьмя стандартными. К примеру, можно создать новый уровень логирования между Debug и Info с целыми значениями в интервале (-4;0).
Функции пакета slog схожи с рассмотренными ранее. Для сравнения разберем их на конкретном примере:
В результате выполнения кода в консоль будет выведено следующее:
Синтаксис чередующегося значения ключей для атрибутов удобен, но для часто выполняемых операторов может быть более эффективным использовать тип Attr
с методом LogAttrs
. Такой подход позволит оптимизировать потребление памяти и обеспечить безопасность типов при указании дополнительных атрибутов. Учитывая это, заменим предыдущий код с выводом сообщения “Server started” на аналогичный с использованием LogAttrs:
В консоль будет выведено следующее сообщение:
Настройка обработчиков, включая TextHandler и JSONHandler, производится с помощью типа HandlerOptions:
После выполнения кода будет выведено только сообщение уровня Info с указанием пути к исполняемому файлу:
Подведем итоги
В этой части самоучителя мы расширили наше представление об ошибках, узнали методы их обработки, а также познакомились с механизмами паники и восстановления. Во втором блоке статьи было рассмотрено логирование в Go с использованием трех различных пакетов: log, logrus и slog.
В следующей части погрузимся в парадигму обобщенного программирования и изучим дженерики, а в конце закрепим материал на интересных задачах.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
Комментарии