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

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

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

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

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

Метод Is(error) bool должен вернуть true, когда проверяемая ошибка относится к стабильной публичной категории, представленной целевой ошибкой. Он задаёт семантическое соответствие, поэтому errors.Is сможет распознать ошибку даже при другой внутренней структуре и тексте сообщения.

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

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

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

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

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

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

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

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

Метод Is должен выполнять неглубокое сравнение текущей ошибки с целевой категорией. Обычно он сопоставляет целевую ошибку с публичным sentinel-значением или проверяет фиксированный вид причины; самостоятельно вызывать Unwrap внутри Is не следует.

package main import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") type storageError struct{ kind string } func (e storageError) Error() string { return "storage failure" } func (e storageError) Is(target error) bool { return e.kind == "not_found" && target == ErrNotFound } func load() error { return fmt.Errorf("load object: %w", storageError{kind: "not_found"}) } func main() { fmt.Println(errors.Is(load(), ErrNotFound)) }

Здесь внутренний тип storageError не обязан быть частью публичного API. Метод Is объявляет только стабильный контракт: конкретный экземпляр относится к категории ErrNotFound. Обёртка через %w сохраняет эту возможность для errors.Is и одновременно добавляет контекст.

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

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

Сервис хранения данных возвращает внутренние ошибки драйвера. При смене драйвера меняются типы ошибок, но HTTP-слой должен по-прежнему преобразовывать отсутствие записи в ответ 404.

Первый вариант — экспортировать тип ошибки драйвера. Он позволяет использовать errors.As, но связывает публичный API сервиса с конкретной библиотекой и усложняет замену драйвера.

Второй вариант — возвращать только ErrNotFound. Он сохраняет стабильную классификацию, но теряет контекст операции, идентификатор ресурса и внутреннюю причину.

Выбран вариант с публичным sentinel-значением категории, внутренним типом с методом Is и wrapping через %w. В результате HTTP-слой проверяет errors.Is, получает стабильное поведение при смене драйвера, а журналы сохраняют подробный контекст.

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

1. Может ли метод Is сравнивать ошибку с несколькими категориями?

Да, если это отражает контракт ошибки. Например, одна ошибка может считаться одновременно ошибкой временной недоступности и ошибкой конкретного класса операции. Однако такое соответствие должно быть обоснованным: слишком широкая классификация делает обработку неоднозначной и может привести к неверному retry или неправильному коду ответа.

2. Должен ли метод Is сравнивать целевую ошибку по тексту?

Нет. Текст ошибки не является устойчивым идентификатором и может меняться из-за локализации, уточнения сообщения или изменения реализации. Метод должен сравнивать sentinel-значение, стабильный вид причины или другие формальные признаки, предусмотренные контрактом.

3. Что изменится, если ошибка с методом Is будет обёрнута через %v вместо %w?

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