В обработчике errors.As не находит ошибку, хотя в цепочке есть тот же тип: значение ошибки возвращено по значению, а цель поиска задана как указатель. В чём причина?
errors.As сопоставляет динамический тип ошибки с типом цели по присваиваемости, а не по имени типа. Если в цепочке находится значение LimitError, поиск в переменную типа *LimitError не сработает; для него нужна цель типа LimitError. Если же ошибка создана как *LimitError, тогда поиск в *LimitError успешен.
До появления стандартных errors.Is и errors.As в Go 1.13 разработчики часто извлекали конкретные типы ошибок через прямое приведение или сравнивали их вручную. Такой подход плохо работал с обёртками: добавление контекста скрывало исходный динамический тип.
errors.As появился как единый механизм поиска типизированной ошибки в цепочке wrapping-ошибок. При этом он сохранил важное правило Go: значение и указатель на него — разные динамические типы.
Предположим, функция возвращает error, внутри которого лежит значение конкретного типа. Обработчик ожидает указатель на этот тип, потому что обычно ошибки создаются через указатели.
Если форма цели не соответствует фактическому динамическому типу, errors.As вернёт false, даже если имена типов совпадают и методы ошибки выглядят одинаково. Неверный вывод обработчика может привести к пропуску специальной логики: например, к неправильному HTTP-коду или отсутствию извлечения структурированных полей ошибки.
errors.As(err, target) проходит по ошибке и её цепочке, полученной через Unwrap. Для каждой ошибки проверяется, присваивается ли её динамический тип типу, на который указывает target. При совпадении найденное значение записывается в целевую переменную.
В первом вызове цель имеет тип *LimitError, но фактический тип ошибки — LimitError. Во втором цель указывает на переменную LimitError, поэтому совпадение происходит, а v получает найденное значение.
Синтаксис с адресом цели не означает, что errors.As всегда ищет указатель как фактическую ошибку. &v имеет тип *LimitError, но сама цель поиска — тип LimitError, то есть тип элемента, на который указывает этот адрес.
Если метод Error объявлен только для *LimitError, значение LimitError вообще не реализует интерфейс error. В таком случае в error можно положить только *LimitError, и целью обычно будет var p *LimitError; errors.As(err, &p).
Практическое правило: выберите единый стиль представления каждого публичного типа ошибки — значение или указатель — и используйте такую же форму в обработчиках. Для экспортируемых ошибок часто удобнее указатели, если ошибка содержит изменяемое или объёмное состояние, но это должно быть осознанным соглашением API.
Цель errors.As должна быть ненулевым указателем на переменную интерфейсного типа либо на тип, реализующий error; иначе функция может завершиться паникой. Само приведение не анализирует текст, возвращаемый Error(), и не считает два разных динамических типа эквивалентными только из-за одинаковых сообщений.
Сервис лимитов возвращал структурированную ошибку LimitError по значению. В HTTP-слое обработчик искал *LimitError, поскольку другой пакет создавал ошибки этого типа через указатели. В результате превышение лимита иногда обрабатывалось как внутренняя ошибка сервера.
Рассматривались два варианта. Первый — проверять оба варианта цели: LimitError и *LimitError; это повышает совместимость, но усложняет код и сохраняет неоднозначный контракт. Второй — изменить публичный контракт и всегда возвращать *LimitError; это требует согласованного изменения конструкторов и тестов, зато устраняет двусмысленность.
Был выбран второй вариант: тип ошибки стал создаваться и возвращаться только как указатель, а обработчики использовали errors.As с целью *LimitError. После этого обёртки добавляли контекст без потери структурированной обработки, а форма динамического типа стала частью явно зафиксированного соглашения пакета.
1. Сравнивает ли errors.As текст ошибки или имя типа?
Нет. Текст Error() вообще не участвует в сопоставлении. Имя типа также не сравнивается отдельно: важна присваиваемость фактического динамического значения типу цели, включая реализуемые интерфейсы.
Поэтому две ошибки с одинаковым текстом могут обрабатываться по-разному, а ошибка с другим текстом может успешно находиться через общий интерфейс. Это одна из причин, по которой машинную обработку ошибок нельзя строить на разборе строк.
2. Почему передача самой переменной вместо указателя на неё некорректна?
errors.As должен иметь возможность записать найденную ошибку в целевую переменную. Поэтому ему передают указатель на эту переменную: например, адрес переменной типа LimitError или адрес переменной типа *LimitError.
Передача самой переменной не даёт функции адресуемой цели нужного формата. Если аргумент не является ненулевым указателем на допустимый тип, errors.As не возвращает обычное несовпадение, а вызывает панику из-за нарушения контракта функции.
3. Может ли пользовательский тип изменить стандартное сопоставление?
Да. Тип ошибки может определить метод As(any) bool и самостоятельно сообщить, что он представим в некотором целевом типе, при необходимости заполнив цель. Это полезно для совместимости разных реализаций одной публичной категории ошибки.
Такой механизм следует применять осторожно: пользовательский As должен сохранять понятную семантику и корректно работать с допустимыми целями. Иначе обработчик сможет получить тип, который фактически не отражает причину ошибки, а поведение API станет трудно предсказуемым.