Программирование GoОбработка ошибокРазработчик Go среднего уровня

Сравнение: чем отличается направление проверки errors.Is обёртка, причина от обратного порядка аргументов?

Сравнение: чем отличается направление проверки errors.Is(обёртка, причина) от обратного порядка аргументов?

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

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

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

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

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

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

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

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

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

При вызове errors.Is(err, target) сначала проверяется сам err. Затем используется пользовательский метод Is, если он есть, после чего errors.Is переходит к причине через Unwrap. Для ошибки с несколькими причинами обход выполняется по структуре, которую предоставляет Unwrap() []error.

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

package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func main() { wrapped := fmt.Errorf("load user: %w", ErrNotFound) fmt.Println(errors.Is(wrapped, ErrNotFound)) fmt.Println(errors.Is(ErrNotFound, wrapped)) }

Первый вызов печатает true, потому что причина находится внутри wrapped. Второй обычно печатает false: у ErrNotFound нет вложенной причины wrapped.

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

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

Слой хранилища возвращает ошибку с контекстом «чтение пользователя», оборачивая ErrNotFound. HTTP-обработчик должен определить, является ли исходная причина отсутствием записи.

Вариант с анализом текста ненадёжен: текст может измениться при добавлении контекста, локализации или смене реализации. Вариант с переставленными аргументами errors.Is(ErrNotFound, err) также неверен: он ищет обёртку внутри sentinel-ошибки.

Выбранное решение — проверять errors.Is(err, ErrNotFound), где err является фактически полученной ошибкой, а ErrNotFound — искомой категорией. Это сохраняет независимость обработчика от количества слоёв wrapping и позволяет библиотеке менять текст сообщений без нарушения логики.

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

  1. Дополнительный вопрос: является ли errors.Is симметричной проверкой?

Нет. Она направлена от первого аргумента к его вложенным причинам. Даже если два значения логически связаны, перестановка аргументов не сохраняет результат.

  1. Дополнительный вопрос: может ли пользовательский метод Is изменить результат при обратном порядке аргументов?

Да. Метод Is вызывается у текущего элемента цепочки, то есть у первого аргумента или одной из его причин. Поэтому при перестановке аргументов будет вызван другой метод либо метод вообще не будет найден. Это ещё одна причина не проектировать Is с предположением о симметричности.

  1. Дополнительный вопрос: что следует передавать вторым аргументом при проверке типизированной ошибки?

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