1. Обработка ошибок (Errors)
Ошибка в Go указывает на то, что конкретная задача не может быть успешно завершена. Это ожидаемая и обрабатываемая ситуация.
Идиомы обработки ошибок
В отличие от таких языков, как Python, Java или Ruby, которые используют модель исключений (exception) с блоками try/catch, Go использует возможность функций возвращать несколько значений. Стандартной идиомой является возвращение ошибки в качестве последнего возвращаемого значения. Рассмотрим базовый пример функции Concat, которая объединяет строки:
Обработка такой ошибки в вызывающем коде выглядит следующим образом:
Ключевой момент здесь - использование оператора if, который позволяет выполнить присваивание result, err := Concat(...) перед проверкой условия err != nil. Область видимости переменных result и err ограничена этим блоком if/else.
Минимизация использования nil
Возврат полезного значения вместе с ошибкой - это хорошая практика. В примере с Concat даже в случае ошибки возвращается пустая строка, что является логичным результатом. Это позволяет упростить код для тех, кто хочет проигнорировать ошибку (хотя делать это следует с осторожностью):
Совет: Для создания простых ошибок используйте errors.New("message"). Для создания форматированной строки ошибки используйте fmt.Errorf("Error: %s", reason).
Пользовательские типы ошибок
Интерфейс error в Go чрезвычайно прост:
Иногда возникает необходимость передать больше информации, чем просто строка. Для этого можно создать собственный тип, реализующий этот интерфейс.
Например, ошибка парсера, содержащая информацию о строке и символе:
Такой тип позволяет не только вывести сообщение, но и программно проанализировать место ошибки.
Переменные ошибок
Часто функция может столкнуться с несколькими видами ошибок, но не всем им нужны дополнительные атрибуты. В этом случае идиоматическим решением в Go является создание переменных ошибок на уровне пакета.
Рассмотрим пример функции, имитирующей отправку запроса, которая может вернуть одну из двух ошибок:
Этот подход прост и эффективен, так как ошибки создаются только один раз (при инициализации пакета), а их проверка сводится к простому сравнению err == ErrTimeout.
2. Система аварий (Panics)
Авария (panic) сигнализирует о критической, неожиданной ситуации, которая угрожает целостности программы (например, деление на ноль, доступ к элементу массива за его пределами). Необработанная авария приводит к аварийному завершению программы.
Отличия аварий от ошибок
- Ошибка - это ожидаемая ситуация, которую можно и нужно обработать. Она является частью обычного потока выполнения.
- Авария - это неожиданная ситуация, часто вызванная ошибкой программиста. Она нарушает нормальный ход выполнения программы.
Возбуждение аварий
Для возбуждения аварии используется встроенная функция panic(interface{}). В нее можно передать что угодно, но идиоматически правильным является передача значения, реализующего интерфейс error (например, созданного через errors.New). Это упрощает последующую обработку аварии.
Восстановление после аварий
Go предоставляет механизм для перехвата аварии и восстановления нормального выполнения программы. Он основан на связке ключевых слов defer и recover.
- defer - откладывает выполнение функции до момента выхода из текущей функции. Отложенные функции выполняются даже при аварии.
- recover - функция, которую можно вызвать внутри отложенной, чтобы остановить раскрутку стека и получить значение, переданное в panic. Если аварии нет, recover возвращает nil.
Базовый пример восстановления:
Более сложный пример показывает, как восстановиться после аварии, закрыть ресурсы и вернуть ошибку:
Важно: Объявляйте отложенные функции как можно ближе к началу вмещающей функции, чтобы было понятно, какая логика очистки будет выполнена.
Аварии и горутины
Каждая горутина работает в своем собственном стеке вызовов. Авария, возникшая в горутине и не перехваченная внутри нее, приведет к аварийному завершению всей программы, а не только этой горутины.
Рассмотрим простой эхо-сервер:
Авария в response "убьет" всю программу. Чтобы этого избежать, необходимо обрабатывать аварии внутри каждой сопрограммы, где они могут возникнуть.
Для упрощения этой задачи можно создать специальную функцию-обертку для запуска сопрограмм:
Использование:
Эта обертка перехватит любую аварию в сопрограмме и запишет ее в лог, не позволяя ей "уронить" всю программу.
Итоги
Система обработки ошибок и аварий в Go, хотя и кажется на первый взгляд избыточной, способствует созданию надежного и предсказуемого ПО. Она заставляет разработчика явно обращать внимание на возможные проблемы.
- Ошибки - это часть нормального потока выполнения. Используйте множественный возврат, возвращайте осмысленные значения вместе с ошибкой, создавайте переменные ошибок для частых сценариев и пользовательские типы для передачи дополнительного контекста.
- Аварии - это чрезвычайные ситуации. Используйте их для уведомлении о неожиданных ошибках. Всегда передавайте в panic значение типа error.
- Восстанавливайтесь от аварий с помощью defer и recover, чтобы грациозно закрывать ресурсы и, по возможности, преобразовывать аварию в обычную ошибку.
- Помните о горутинах. Авария, не перехваченная внутри горутины, приведет к краху всей программы. Обеспечьте обработку аварий на верхнем уровне каждой горутины, которая в этом нуждается.
Этот подход в итоге приводит к более ясному, контролируемому и отказоустойчивому коду.
Жду ваших комментариев! Поставьте лайк, если хотите видеть больше интересного контента.
До скорой встречи!