При выполнении любой программы могут возникать ошибки. В одних случаях — из-за неверных входных данных или отсутствия доступа к системным ресурсам. В других — из-за плохо написанного кода, способствующего аварийным ситуациям и прекращению работы программы.
Рассмотрим на примере. Предположим, программа запрашивает у пользователя два числа и делит одно на другое. Очевидно, нужно проверить, не равен ли делитель нулю. Если такая проверка есть и пользователь вводит ноль, достаточно попросить его ввести другое число. Если проверки нет, работа программы завершится аварийной ситуацией.
Ни один язык программирования не гарантирует, что ошибок не возникнет, поэтому в любом языке найдутся инструменты для их обработки. Например, один из подходов — использование механизма исключений. Можно перехватывать ошибки в любом месте программы и решать, что делать с ними дальше.
Note
В Go подход к обработке ошибок отличается. Программа получает ошибку в момент возникновения и должна сразу её обработать. Это требует дополнительного кода, но уменьшает количество аварийных ситуаций.
Тип error
Ранее мы уже упоминали тип error
вскользь. Действительно, конструкция if err != nil {}
является чуть ли не визитной карточкой языка и встречается в каждой программе на Go.
Тип error
— это интерфейсный тип:
Благодаря тому, что функции могут возвращать несколько значений, ошибки легко попадают в их ряды. Использование error
в последнем возвращаемом значении функции — очень распространённый в Go паттерн. Если возвращаемое значение ошибки равно nil
, то функция завершилась корректно. В противном случае остальные возвращенные значения функции использовать нельзя и нужно обработать ошибку.
Рассмотрим пример:
Обычно в Go используется паттерн «ранний выход», поэтому код выше лучше использовать так:
Если возникла ошибка, то, скорее всего, продолжать нет смысла, и лучше завершить работу как можно раньше. Кроме того, это не замусоривает код лишними else
и фигурными скобками, делает его более идиоматичным.
Если запустить такой код с отсутствующим файлом nothing.txt
, программа выведет open nothing.txt: no such file or directory
. Возвращаемое значение data
никак не обрабатывается.
В стандартной библиотеке Go есть пакет errors
для работы с ошибками. Чтобы создать переменную типа error
, нужно вызвать функцию New
, которая принимает в параметре строку. Например, в коде выше можем создать собственную ошибку, а не возвращать полученную из функции.
Note
errors.New()
не принимает форматированную строку с параметрами, только одну строку с сообщением.
Однако такой подход имеет свои недостатки. В пакете несколько функций могут возвращать одинаковые ошибки. Кроме того, вызов функции формирования ошибки будет происходить каждый раз при её использовании, и переменная ошибки будет пересоздаваться многократно.
Поэтому наиболее часто используется создание ошибок статически, на этапе инициализации модуля:
Если нужно сформировать ошибку с использованием дополнительной информации, можно применять функцию fmt.Errorf
, которая работает как fmt.Sprintf
, но возвращает вместо строки ошибку. В Go принято начинать текст ошибок со строчной буквы, так как ошибки могут объединяться друг с другом.
Сравнение ошибок
Практически любая функция может вернуть разные ошибки. Хотелось бы уметь их анализировать и понимать, что происходит, для последующей работы. Например, база данных может возвращать ошибку NoRows
, если искомые данные не найдены, однако это не всегда является нарушением логики работы программы.
Ошибки можно сравнивать так же, как и другие переменные:
Однако, нельзя сравнивать ошибку ни с чем, кроме как с nil
, если она была сформирована динамически.
Далее узнаем, как разрешить эту проблему.
Обёртывание ошибок
В предыдущем разделе мы увидели два подхода к созданию ошибок: статический и динамический. Оба подхода имеют недостатки.
Статические ошибки быстро обрабатываются и легко читаются, их просто сравнивать, но они теряют гибкость, если нужно добавить в ошибку дополнительную информацию. Динамические ошибки создаются в процессе работы программы, они более медленные, и их трудно сравнивать.
Начиная с версии 1.13, Go даёт возможность «упаковывать», или «обёртывать», ошибки (wrapping error). Это означает, что можно создавать новую ошибку поверх старой, но сохранять возможность восстановить оригинальную ошибку. Это может понадобиться для создания собственных типов ошибок на основе уже существующих. Ниже приводится пример.
Вспомните пример с чтением файла конфигурации, где происходит замена одной ошибки на другую. Текст оригинальной ошибки остаётся, но восстановить её тип невозможно. Чтобы обернуть ошибку, нужно использовать спецификатор %w
для функции Errorf
.
Исправим %v
на %w
в функции ReadTextFile
:
В этом случае можно восстановить оригинальную ошибку, используя функцию errors.Unwrap
, и добавить дополнительные проверки.
Кроме того, в пакете github.com/pkg/errors
есть функции Wrap
и Wrapf
, которые создают обёрнутую ошибку.
Для чего нужно оборачивать ошибки? Дело в том, что с помощью обёртывания можно создать некоторую иерархию ошибок:
А теперь в месте вызова нашей функции сравним:
Функция Is
сравнивает ошибки, причём даже обёрнутые! Благодаря этому можно обрабатывать обёрнутые ошибки и передавать в них какую-то дополнительную информацию.
Хорошим тоном для архитектуры считается, чтобы пакет возвращал только свои ошибки. Набор ошибок пакета является частью контракта по работе с ним. Пробрасывая полученные ошибки из других пакетов, увеличиваем связность пакетов. Ведь тогда стороне, обрабатывающей ошибки, придётся обрабатывать не только ошибки нашего пакета, но и пакетов, от которых зависим.
Cобственные типы ошибок
Иногда в ошибке, кроме текста, нужно передать дополнительную информацию. Разберём пример, в котором вместе с текстом ошибки будем возвращать время её возникновения:
Cобственные типы ошибок
Иногда в ошибке, кроме текста, нужно передать дополнительную информацию. Разберём пример, в котором вместе с текстом ошибки будем возвращать время её возникновения:
Получим примерно следующее:
2021/05/28 11:33:00: Параметр в testFunc равен 0
Если ошибкой может выступать переменная любого интерфейсного типа error
, значит, можно использовать операцию утверждения типа (type assertion) для конвертации ошибки в конкретный базовый тип.
Если ошибки могут быть разных типов, логично использовать конструкцию выбора типа:
Но лучше применить функцию As
пакета errors
, так как она, в отличие от type assertion
, работает с «обёрнутыми» ошибками, которые разберём ниже. As
находит первую в цепочке ошибку err
, устанавливает тип, равным этому значению ошибки, и возвращает true
.
Возвращение ошибки не всегда означает, что ситуация критическая. Ошибка может сообщать о статусе или состоянии какого-то действия или ресурса. Например, при проверке наличия файла нужно дополнительно проверить полученную ошибку функцией os.IsNotExist
. Другой пример — чтение из источника должно продолжаться до получения ошибки io.EOF
, которая сигнализирует о том, что все данные прочитаны.
В данном примере можно использовать функцию Is(err, target error) bool
из пакета errors
, которая определяет, содержит ли цепочка ошибок конкретную ошибку.
📂 Go | Последнее изменение: 03.09.2024 12:47