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

Какой метод должен предоставить пользовательский тип ошибки, чтобы errors.Is продолжил поиск исходной причины?

Какой метод должен предоставить пользовательский тип ошибки, чтобы errors.Is продолжил поиск исходной причины?

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

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

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

Если метод возвращает nil, цепочка заканчивается. Само наличие Unwrap не делает ошибки равными: оно лишь сохраняет доступ к исходной причине.

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

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

Начиная с Go 1.13 стандартная библиотека получила согласованный механизм цепочек ошибок: Unwrap, errors.Is, errors.As и форматирование с %w. Он отделяет сообщение для человека от машинно проверяемой структуры причины.

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

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

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

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

Тип ошибки, который добавляет контекст к одной причине, реализует Error() string и Unwrap() error. Error формирует понятное сообщение, а Unwrap возвращает именно ту ошибку, которую тип оборачивает.

package main import ( "errors" "fmt" ) type OpError struct { Op string; Err error } func (e *OpError) Error() string { return e.Op + ": " + e.Err.Error() } func (e *OpError) Unwrap() error { return e.Err } var ErrNotFound = errors.New("not found") func main() { err := &OpError{"load user", ErrNotFound} fmt.Println(errors.Is(err, ErrNotFound)) // true }

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

Метод Unwrap должен возвращать реальную причину, а не новую ошибку с тем же текстом. Возврат новой ошибки разрушит ожидаемую идентичность или типовую связь; сравнение строк эту проблему не решает.

Для одной причины используется сигнатура Unwrap() error. В современных версиях Go возможна модель нескольких причин через Unwrap() []error, которую обходят errors.Is и errors.As; у одного типа нельзя одновременно объявить обе версии метода из-за отсутствия перегрузки методов.

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

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

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

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

Выбран третий вариант. На границе HTTP-сервиса результат errors.Is преобразуется в безопасный публичный ответ, а внутренняя цепочка остаётся доступной для логирования и обработки внутри приложения. Это позволяет не раскрывать детали драйвера и одновременно не терять причину внутри системы.

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

  1. Достаточно ли реализовать Error, чтобы errors.Is нашёл исходную ошибку?

Нет. Error предоставляет только текстовое представление. Без Unwrap() error стандартный обход не узнает, что внутри пользовательской ошибки находится другая причина, поэтому проверка вложенного sentinel обычно вернёт false.

  1. Что произойдёт, если Unwrap вернёт ошибку с тем же текстом, но созданную заново?

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

  1. Делает ли Unwrap вложенную ошибку частью публичного API пакета?

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

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