Программирование GoОбработка ошибокGo-разработчик серверных приложений

Как по цепочке обёрнутых ошибок определить, что причина относится к конкретному типу, не сравнивая тексты с...

Как по цепочке обёрнутых ошибок определить, что причина относится к конкретному типу, не сравнивая тексты сообщений?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

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

Целевой объект обычно передают через указатель на переменную-указатель: сначала объявляют переменную нужного типа, затем передают в errors.As её адрес.

Исторический контекст

Изначально ошибки в Go передавались как значения интерфейса error, а их текст часто использовался как единственный способ различать причины. Такой подход был хрупким: изменение формулировки сообщения ломало обработчики, а добавление контекста затрудняло проверку исходной ошибки.

В Go 1.13 появились стандартизованные wrapping ошибок и функции errors.Is и errors.As. Они решили задачу сохранения контекста при передаче ошибки вверх по стеку, одновременно предоставив машинно проверяемый способ распознавать её семантику.

Постановка проблемы

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

Проверка текста ненадёжна, а сравнение через обычное приведение верхнего уровня не обнаруживает тип внутри обёртки. Неверное решение может привести к неправильной реакции: временную ошибку сочтут окончательной, либо внутренний сбой будет ошибочно скрыт повторной попыткой.

Подробное решение

errors.As последовательно проверяет саму ошибку и ошибки, доступные через метод Unwrap. При первом совпадении она записывает найденное значение в целевую переменную и возвращает true. Обёртка при этом не уничтожает исходный тип, если она корректно создана с поддержкой wrapping.

Минимальный пример:

package main import ( "errors" "fmt" "time" ) type RateLimitError struct{ RetryAfter time.Duration } func (e *RateLimitError) Error() string { return "rate limit exceeded" } func load() error { return fmt.Errorf("request failed: %w", &RateLimitError{time.Second}) } func main() { var limit *RateLimitError if errors.As(load(), &limit) { fmt.Println(limit.RetryAfter) } }

Здесь fmt.Errorf с маркером %w сохраняет исходную ошибку в цепочке, а errors.As извлекает её как RateLimitError. Передача &limit важна: функции нужен адрес переменной, в которую она сможет записать найденное значение.

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

errors.As не означает, что любую ошибку нужно раскрывать наружу. Типы ошибок формируют контракт между слоями: экспортируемый тип можно использовать для программной обработки, а внутренний тип лучше не делать частью внешнего API без необходимости.

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

Для проверки конкретного заранее известного значения применяется errors.Is, а не errors.As. Смешение этих задач делает код менее понятным: Is отвечает на вопрос о тождестве или семантическом совпадении, As — о получении значения определённого типа или интерфейса.

Ситуация из практики

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

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

Выбран errors.As вместе с корректным wrapping через %w. Обработчик извлекает типизированную ошибку, читает её структурированные данные и преобразует их в стабильный внешний ответ. В результате внутренний контекст сохраняется, повторные попытки запускаются только для подходящей причины, а изменение текста ошибки не ломает логику.

Что кандидаты часто упускают

  1. Чем отличается errors.As от обычного приведения типа?

Обычное приведение проверяет только значение, находящееся непосредственно в интерфейсе ошибки. Если ошибка была обёрнута, верхний объект имеет тип обёртки, поэтому прямое приведение не найдёт внутреннюю причину. errors.As дополнительно обходит цепочку через Unwrap, сохраняя возможность анализировать ошибку после добавления контекста.

  1. Почему передача неправильной цели в errors.As является ошибкой?

Цель должна быть ненулевым указателем на тип ошибки или интерфейс, в который разрешено записать совпадение. Поэтому для указательного типа обычно передают адрес переменной-указателя; передача самого указателя не оставляет функции корректного места для записи и может привести к панике из-за неверной формы аргумента. Практически это означает, что нужно заранее проверить тип цели и не строить её динамически без необходимости.

  1. Когда тип ошибки не следует экспортировать даже при использовании errors.As?

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