Как стать автором
Обновить
1320.22
OTUS
Цифровые навыки от ведущих экспертов

Обработка ошибок в Go

Время на прочтение8 мин
Количество просмотров26K
Автор оригинала: Alon Abadi
Привет, хабровчане! Уже сегодня в ОТУС стартует курс «Разработчик Golang» и мы считаем это отличным поводом, чтобы поделиться еще одной полезной публикацией по теме. Сегодня поговорим о подходе Go к ошибкам. Начнем!



Освоение прагматической обработки ошибок в вашем Go-коде




Этот пост является частью серии «Перед тем как приступать к Go», где мы исследуем мир Golang, делимся советами и идеями, которые вы должны знать при написании кода на Go, чтобы вам не пришлось набивать собственные шишки.

Я предполагаю, что у вас уже имеется хотя бы базовый опыт работы с Go, но если вы чувствуете, что в какой-то момент вы столкнулись с незнакомым обсуждаемым материалом, не стесняйтесь делать паузу, исследовать тему и возвращаться.

Теперь, когда мы расчистили себе путь, поехали!

Подход Go к обработке ошибок — одна из самых спорных и неправильно используемых фич. В этой статье вы узнаете подход Go к ошибкам, и поймете, как они работают “под капотом”. Вы изучите несколько различных подходов, рассмотрите исходный код Go и стандартную библиотеку, чтобы узнать, как обрабатываются ошибки и как с ними работать. Вы узнаете, почему утверждения типа (Type Assertions) играют важную роль в их обработке, и увидите предстоящие изменения в обработке ошибок, которые планируется ввести в Go 2.



Вступление


Сперва-наперво: ошибки в Go не являются исключениями. Дэйв Чейни написал эпический блогпост об этом, поэтому я отсылаю вас к нему и резюмирую: на других языках вы не можете быть уверены, может ли функция вызвать исключение или нет. Вместо генерации исключений функции Go поддерживают множественные возвращаемые значения, и по соглашению эта возможность обычно используется для возврата результата функции наряду с переменной ошибки.



Если по какой-то причине ваша функция может дать сбой, вам, вероятно, следует вернуть из нее предварительно объявленный error-тип. По соглашению, возврат ошибки сигнализирует вызывающей стороне о проблеме, а возврат nil не считается ошибкой. Таким образом, вы дадите вызывающему понять, что возникла проблема, и ему нужно разобраться с ней: кто бы ни вызвал вашу функцию, он знает, что не должен полагаться на результат до проверки на наличие ошибки. Если ошибка не nil, он обязан проверить ее и обработать (логировать, возвращать, обслуживать, вызвать какой-либо механизм повторной попытки/очистки и т. д.).


(3 // обработка ошибки
5 // продолжение)


Эти фрагменты очень распространены в Go, и некоторые рассматривают их в качестве шаблонного кода. Компилятор рассматривает неиспользуемые переменные как ошибки компиляции, поэтому, если вы не собираетесь проверять наличие ошибок, вы должны назначить их пустому идентификатору. Но как бы удобно это ни было, ошибки не следует игнорировать.


( 4 // игнорирование ошибок небезопасно, и вы не должны полагаться на результат прежде, чем проверите наличие ошибок)
результату нельзя доверять до проверки на наличие ошибок

Возврат ошибки вместе с результатами, наряду со строгой системой типов Go, значительно усложняет написание забагованного кода. Вы всегда должны полагать, что значение функции повреждено, если только вы не проверили ошибку, которую она возвратила, и, присваивая ошибку пустому идентификатору, вы явно игнорируете, что значение вашей функции может быть повреждено.


Пустой идентификатор темен и полон ужасов.

У Go действительно есть panic и recover механизмы, которые также описаны в другом подробном посте в блоге Go. Но они не предназначены для имитации исключений. По словам Дейва, «Когда вы паникуете в Go — вы действительно паникуете: это не проблема кого-то другого, это уже геймовер». Они фатальны и приводят к сбою в вашей программе. Роб Пайк придумал поговорку «Не паникуйте», которая говорит сама за себя: вам, вероятно, следует избегать эти механизмы и вместо них возвращать ошибки.

«Ошибки — значения».
«Не просто проверяйте наличие ошибок, а элегантно их обрабатывайте»
«Не паникуйте»
все поговорки Роба Пайка

Под капотом


Интерфейс ошибки

Под капотом тип error — это простой интерфейс с одним методом, и если вы с ним не знакомы, я настоятельно рекомендую просмотреть этот пост в официальном блоге Go.


интерфейс error из исходного кода

Свои собственные ошибки реализовать не сложно. Существуют различные подходы к пользовательским структурам, реализующим метод Error() string . Любая структура, реализующая этот единственный метод, считается допустимым значением ошибки и может быть возвращена как таковая.

Давайте рассмотрим несколько таких подходов.

Встроенная структура errorString


Наиболее часто используемая и широко распространенная реализация интерфейса ошибок — это встроенная структура errorString . Это самая простая реализация, о которой вы только можете подумать.


Источник: исходный код Go

Вы можете лицезреть ее упрощенную реализацию здесь. Все, что она делает, это содержит string, и эта строка возвращается методом Error. Эта стринговая ошибка может быть нами отформатирована на основе некоторых данных, скажем, с помощью fmt.Sprintf. Но кроме этого, она не содержит никаких других возможностей. Если вы применили errors.New или fmt.Errorf, значит вы уже использовали ее.


(13// вывод:)

попробуйте

github.com/pkg/errors


Другой простой пример — пакет pkg/errors. Не путать со встроенным пакетом errors, о котором вы узнали ранее, этот пакет предоставляет дополнительные важные возможности, такие как обертка ошибок, развертка, форматирование и запись стек-трейса. Вы можете установить пакет, запустив go get github.com/pkg/errors.



В тех случаях, когда вам нужно прикрепить стек-трейс или необходимую информацию об отладке к вашим ошибкам, использование функций New или Errorf этого пакета предоставляет ошибки, которые уже записываются в ваш стек-трейс, и вы так же можете прикрепить простые метаданные, используя его возможности форматирования. Errorf реализует интерфейс fmt.Formatter, то есть вы можете отформатировать его, используя руны пакета fmt ( %s, %v, %+v и т. д.).


(//6 или альтернатива)

Этот пакет также представляет функции errors.Wrap и errors.Wrapf. Эти функции добавляют контекст к ошибке с помощью сообщения и стек-трейса в том месте, где они были вызваны. Таким образом, вместо простого возврата ошибки, вы можете обернуть ее контекстом и важными отладочными данными.



Обертки ошибок другими ошибками поддерживают Cause() error метод, который возвращает их внутреннюю ошибку. Кроме того, они могут использоваться сerrors.Cause(err error) error функцией, которая извлекает основную внутреннюю ошибку в оборачивающей ошибке.

Работа с ошибками


Утверждение типа


Утверждения типа (Type Assertion) играют важную роль при работе с ошибками. Вы будете использовать их для извлечения информации из интерфейсного значения, а поскольку обработка ошибок связана с пользовательскими реализациями интерфейса error, реализация утверждений на ошибках является очень удобным инструментом.

Его синтаксис одинаков для всех его целей —  x.(T), если  x имеет тип интерфейса.  x.(T) утверждает, что  x не равен nil и что значение, хранящееся в x, относится к типу T. В следующих нескольких разделах мы рассмотрим два способа использования утверждений типа — с конкретным типом T и с интерфейсом типа T.


(2//сокращенный синтаксис, пропускающий логическую переменную ok
3//паника: преобразование интерфейса: интерфейс {} равен nil, а не string
6//удлиненный синтаксис с логической переменной ok
8//не паникует, вместо этого присваивает ok false, когда утверждение ложно
9// теперь мы можем безопасно использовать s как строку)


песочница: panic при укороченном синтаксисе, безопасный удлинённый синтаксис

Дополнительное примечание, касающееся синтаксиса: утверждение типа может использоваться как с укороченным синтаксисом (который паникует при неудачном утверждении), так и с удлиненным синтаксисом (который использует логическое значение OK для указания успеха или неудачи). Я всегда рекомендую брать удлиненный вместо укороченного, так как я предпочитаю проверять переменную OK, а не разбираться с паникой.


Утверждение с интерфейсом типа T


Выполнение утверждения типа x.(T) с интерфейсом типа T подтверждает, что x реализует интерфейс T. Таким образом, вы можете гарантировать, что интерфейсное значение реализует интерфейс, и только если это так, вы сможете использовать его методы.


(5...// утверждаем, что x реализует интерфейс resolver
6...// здесь мы уже можем безопасно использовать этот метод)


Чтобы понять, как это можно использовать, давайте снова взглянем на pkg/errors. Вы уже знаете этот пакет ошибок, так что давайте углубимся в errors.Cause(err error) error функцию.

Эта функция получает ошибку и извлекает самую внутреннюю ошибку, которую та переносит (ту, которая уже не служит оберткой для другой ошибки). Это может показаться примитивным, но есть много замечательных вещей, которые вы можете извлечь из этой реализации:


источник: pkg/errors

Функция получает значение ошибки, и она не может предполагать, что получаемый ею err аргумент является ошибкой-оберткой (поддерживаемой Cause методом). Поэтому перед вызовом метода Cause необходимо убедиться, что вы имеете дело с ошибкой, которая реализует этот метод. Выполняя утверждение типа в каждой итерации цикла for, вы можете убедиться, что cause переменная поддерживает метод Cause, и может продолжать извлекать из него внутренние ошибки до тех пор, пока не найдете ошибку, у которой нет Cause.

Создавая простой локальный интерфейс, содержащий только те методы, которые вам нужны, и применяя на нем утверждение, ваш код отделен от других зависимостей. Полученный вами аргумент не обязательно должен быть известной структурой, он просто должен быть ошибкой. Любой тип, реализующий методы Error и Cause, подойдет. Таким образом, если вы реализуете Cause метод в своем типе ошибки, вы можете использовать эту функцию с ним без замедлений.

Однако следует помнить об одном небольшом недостатке: интерфейсы могут изменяться, поэтому вам следует тщательно поддерживать код, чтобы ваши утверждения не нарушались. Не забудьте определить свои интерфейсы там, где вы их используете, поддерживать их стройными и аккуратными, и у вас все будет хорошо.

Наконец, если вам нужен только один метод, иногда удобнее сделать утверждение на анонимном интерфейсе, содержащем только метод, на который вы полагаетесь, т. е. v, ok := x.(interface{ F() (int, error) }). Использование анонимных интерфейсов может помочь отделить ваш код от возможных зависимостей и защитить его от возможных изменений в интерфейсах.

Утверждение с конкретным типом T и Type Switch



Я предваряю этот раздел введением двух похожих шаблонов обработки ошибок, которые страдают от нескольких недостатков и ловушек. Это не значит, что они не распространенные. Оба они могут быть удобными инструментами в небольших проектах, но они плохо масштабируются.

Первый — это второй вариант утверждения типа: выполняется утверждение типа x.(T) с конкретным типом T. Он утверждает, что значение x имеет тип T, или оно может быть преобразовано в тип T.


(2//мы можем использовать v как mypkg.SomeErrorType)

Другой — это шаблон Type Switch. Type Switch объединяют оператор switch с утверждением типа, используя зарезервированное ключевое слово type. Они особенно распространены в обработке ошибок, где знание основного типа переменной ошибки может быть очень полезным.


(3// обработка…
5// обработка...)


Большим недостатком обоих подходов является то, что оба они приводят к связыванию кода со своими зависимостями. Оба примера должны быть знакомы со структурой SomeErrorType (которая, очевидно, должна быть экспортирована) и должны импортировать пакет mypkg.
В обоих подходах при обработке ваших ошибок вы должны быть знакомы с типом и импортировать его пакет. Ситуация усугубляется, когда вы имеете дело с ошибками в обертках, где причиной ошибки может быть ошибка, возникшая из-за внутренней зависимости, о которой вы не знаете и не должны знать.


(7// обработка…
9// обработка...)


Type Switch различают *MyStruct и MyStruct. Поэтому, если вы не уверены, имеете ли вы дело с указателем или фактическим экземпляром структуры, вам придется предоставить оба варианта. Более того, как и в случае с обычными switch, кейсы в Type Switch не проваливаются, но в отличие от обычных Type Switch, использование fallthrough запрещено в Type Switch, поэтому вам придется использовать запятую и предоставлять обе опции, что легко забыть.



Подытожим


Вот и все! Теперь вы знакомы с ошибками и должны быть готовы к устранению любых ошибок, которые ваше приложение Go может выбросить (или фактически вернуть) на ваш путь!
Оба пакета errors представляют простые, но важные подходы к ошибкам в Go, и, если они удовлетворяют вашим потребностям, они являются отличным выбором. Вы можете легко реализовать свои собственные структуры ошибок и пользоваться преимуществами обработки ошибок Go, комбинируя их с pkg/errors.

Когда вы масштабируете простые ошибки, правильное использование утверждений типа может быть отличным инструментом для обработки различных ошибок. Либо с помощью Type Switch, либо путем утверждения поведения ошибки и проверки интерфейсов, которые она реализует.

Что дальше?


Обработка ошибок в Go сейчас очень актуальна. Теперь, когда вы получили основы, вам может быть интересно, что ждет нас в будущем для обработки ошибок Go!

В следующей версии Go 2 этому уделяется много внимания, и вы уже можете взглянуть на черновой вариант. Кроме того, во время dotGo 2019 Марсель ван Лохуизен провел отличную беседу на тему, которую я просто не могу не рекомендовать — «Значения ошибок GO 2 уже сегодня».

Очевидно, есть еще множество подходов, советов и хитростей, и я не могу включить их все в один пост! Несмотря на это, я надеюсь, что вам он понравился, и я увижу вас в следующем выпуске серии «Перед тем как приступать к Go»!

А теперь традиционно ждем ваши комментарии.
Теги:
Хабы:
Всего голосов 23: ↑19 и ↓4+15
Комментарии9

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS