В Go используется термин обработка ошибок и подход к ней серьезно отличается от практикующихся в других широко используемых языках программирования. Этот подход часто критикуют, но и хвалят его не реже.
Проверка возвращаемых ошибок
Рассмотрим базовую обработку ошибок в Go на примере функции, вычисляющей частное хранящихся в двух переменных типа string
чисел:
Функция strconv.Atoi
конвертирует строку в целое число. Переданный ей параметр может оказаться и не числом, поэтому функция возвращает два значения: первое – результат при успешном выполнении; второе – значение ошибки, если она возникла. После вызова функции проверяется, произошла ли ошибка и если да, производятся следующие действия:
- К ошибке добавляется дополнительная информация, которая будет полезна для поиска причины ее появления (с помощью функции
fmt.Errorf
и специальной последовательности символов%w
). - Функция прерывает нормальное выполнение и возвращает ошибку как значение (в Go принято возвращать ее последним значением).
Если проблем не возникло, функция продолжит работу и по завершении вернет результат (если он есть) и пустое значение ошибки. Есть и другой вариант: к примеру, strconv.Itoa
преобразует число в строку и не возвращает ошибок.
Механизм паники
Когда проблема становится критичной и дальнейшее нормальное выполнение программы невозможно, используется механизм паники. В предыдущем примере паника возникнет, если делитель равен нулю. Если ничего не предпринять, приложение будет завершено. Чтобы избежать этого, нужно добавить следующую проверку:
где
По сути все сводится к возвращению из функций значений ошибок и последующей их проверке. Явная обработка упрощает разрешение проблемных ситуаций, но требует добавления многословного повторяющегося кода проверки на err != nil
после вызова почти каждой функции или метода. Это увеличивает количество строк в исходных текстах и создает помехи в понимании основной логики кода. Обработка ошибок в такой форме вызывает негодование у привыкших к более традиционному подходу обработки исключений программистов.
Если в Go добавить исключения
При этом пропадает нужда в проверке на возникновение ошибки при каждом вызове функции. Если представить, что в Go когда-нибудь появятся исключения, они будут выглядеть следующим образом:
Пропала проверка на возникновении ошибки. Функция strconv.Atoi
теперь возвращает только одно значение и при проблемной ситуации вместо возврата ошибки бросает исключение с помощью оператора throw
:
где
Деление на ноль в этом случае также порождает исключение. Вызывающий эту функцию код выглядел бы следующим образом:
В блоке try
код способный породить исключение. Если исключение будет брошено, оно перехватится одним из блоков catch
и будет выполнен соответствующий типу исключения код. Если для типа исключения (или переменной в нашем случае) не найден подходящий блок catch
, исключение поднимается дальше в вызывающую функцию и далее до тех пор, пока подходящий catch
не будет найден или программа (поток) не завершится. Функция divide
стала бы короче и легче для чтения.
Реализация исключений через панику
Конечный результат будет выглядеть так:
Чтобы это работало, нужно также изменить функции следующим образом:
И код функции Try
:
recover
. Более того, благодаря встроенной функции panic
ее несложно и вызвать. Функция принимает один аргумент типа interface{}
, который можно будет получить после вызова recover
. Функция recover
должна вызываться в отложенной через defer
функции, так как только отложенные функции исполняются даже при панике.Используя эти знания, мы можем поступить так: при возникновении проблемы искусственно вызывать панику, передавая ей в качестве аргумента произошедшую ошибку. Чтобы отловить эту ошибку будем использовать вспомогательную функцию Try
. Первым аргументом передаем анонимную функцию с нашим кодом, а вторым – функцию обработки любой возникшей ошибки. Если функция в первом аргументе паникует, то Try
перехватывает панику через recover
, извлекает из возвращаемого значения тип error
и передает в нашу функцию обработчик.
Подход имитирует обработку исключений в Go. Функцию Try
можно модифицировать таким образом, чтобы она принимала много отдельных обработчиков ошибок и сама вызывала нужный, хотя реализовать это довольно сложно. Для удобства и возможности повторного использования, функцию Try
стоит вынести в отдельный пакет и импортировать через dot import: например, import . "exception/try"
.
Теперь у нас есть возможность использовать подход обработки исключений, но так ли все хорошо на самом деле? Обработка исключений имеет и свои минусы, хотя она отделена от логики программы и позволяет писать более короткий код.
Нужны ли в Go исключения?
Из дополнительных побочных эффектов исключений стоит отметить потерю производительности. Именно поэтому разработчики игр отказываются от них в пользу альтернативных решений.
Писать код на основе исключений в Go в определенной мере возможно, но в сообществе это даже порицается. Пытаясь использовать такой подход, вы столкнетесь с проблемой экосистемы языка. Стандартная библиотека и сторонние пакеты используют обработку ошибок в форме возвращаемых значений, а смешение методов усложнит написание кода вдвойне.
Обработка ошибок – важная часть работы программиста и какой бы метод вы бы не применяли, тщательно продумывайте поведение программы в проблемных ситуациях. Удачи!
Полный листинг кода:
Комментарии