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

При вызове errors.Is ошибка и искомая цель обе имеют метод Is: чей метод участвует в сопоставлении?

При вызове errors.Is ошибка и искомая цель обе имеют метод Is: чей метод участвует в сопоставлении?

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

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

Вызов errors.Is(err, target) проверяет метод Is текущей ошибки err, а не метод объекта target. Поэтому специальное правило сопоставления должно быть реализовано у ошибки, которую анализируют, обычно у обёртки или ошибки-категории.

Метод Is у target сам по себе не вызывается. Это делает сопоставление направленным: errors.Is(a, b) не обязано давать тот же результат, что errors.Is(b, a).

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

До появления errors.Is разработчики часто сравнивали ошибки напрямую или анализировали текст сообщения. Такой подход плохо работал при добавлении контекста: обёртка меняла значение ошибки, а текст не был надёжным API-контрактом.

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

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

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

В результате проверка вернёт false, хотя у целевой ошибки есть метод Is, который теоретически мог бы вернуть true. Это особенно опасно при проектировании публичных ошибок: неправильное направление сопоставления ломает обработчиков, повторные попытки и классификацию отказов.

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

Алгоритм errors.Is последовательно рассматривает текущую ошибку и её причины. Для каждой ошибки он сначала может проверить прямое совпадение, затем вызвать метод вида Is(error) bool у самой рассматриваемой ошибки и только после этого перейти к причине через Unwrap.

Минимальная демонстрация направленности:

package main import ( "errors" "fmt" ) type source struct{} func (*source) Error() string { return "source" } func (*source) Is(error) bool { return false } type category struct{} func (*category) Error() string { return "category" } func (*category) Is(error) bool { return true } func main() { fmt.Println(errors.Is(&source{}, &category{})) }

Результат — false: метод Is у category, переданной как цель, не вызывается. Вызывается source.Is(category), который возвращает false.

Обычно метод Is у ошибки реализуют так, чтобы она распознавалась как заранее определённая категория или совместимый шаблон. Метод должен выполнять неглубокое сопоставление самой ошибки; обход цепочки причин оставляет errors.Is.

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

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

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

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

Первый — реализовать Is только у публичной sentinel-ошибки и передавать её в errors.Is как цель. Это не работает: errors.Is не вызывает метод цели, поэтому внутренний тип не будет распознан через такое правило.

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

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

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

  1. Является ли errors.Is симметричной операцией?

Нет. errors.Is(a, b) и errors.Is(b, a) могут дать разные результаты, потому что пользовательский метод Is вызывается у текущего элемента цепочки, а не у цели. Поэтому API ошибки должен документировать, какие категории распознаёт сама ошибка, а не предполагать взаимное сопоставление типов.

  1. Должен ли пользовательский Is сам обходить Unwrap?

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

  1. Можно ли реализовать Is у sentinel-ошибки, чтобы она распознавала другие ошибки?

Такое правило не поможет при обычном вызове errors.Is(other, sentinel), поскольку метод sentinel-ошибки находится у цели и не вызывается. Для распознавания категории правило должно находиться у other, у его обёртки или у типа, который фактически проходит в цепочке как текущая ошибка.