При вызове errors.Is ошибка и искомая цель обе имеют метод Is: чей метод участвует в сопоставлении?
Вызов 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.
Минимальная демонстрация направленности:
Результат — 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 или журналирование.
errors.Is симметричной операцией?Нет. errors.Is(a, b) и errors.Is(b, a) могут дать разные результаты, потому что пользовательский метод Is вызывается у текущего элемента цепочки, а не у цели. Поэтому API ошибки должен документировать, какие категории распознаёт сама ошибка, а не предполагать взаимное сопоставление типов.
Is сам обходить Unwrap?Обычно нет. Стандартный механизм сам продолжает обход после проверки текущей ошибки. Если метод Is вручную исследует причины, он дублирует алгоритм, может нарушить ожидаемый порядок поиска и усложнить поддержку, особенно для ошибок с несколькими причинами.
Is у sentinel-ошибки, чтобы она распознавала другие ошибки?Такое правило не поможет при обычном вызове errors.Is(other, sentinel), поскольку метод sentinel-ошибки находится у цели и не вызывается. Для распознавания категории правило должно находиться у other, у его обёртки или у типа, который фактически проходит в цепочке как текущая ошибка.