Программирование GoОбработка ошибокРазработчик Go-библиотек

Добавление метода Unwrap к уже публичному типу ошибки: какое наблюдаемое изменение это может внести в повед...

Добавление метода Unwrap к уже публичному типу ошибки: какое наблюдаемое изменение это может внести в поведение вызывающего кода?

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

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

Добавление Unwrap может сделать внутреннюю причину ошибки доступной для errors.Is и errors.As. Поэтому проверки, которые раньше возвращали false или не находили тип, начнут успешно проходить; внутренний тип ошибки фактически станет частью наблюдаемого поведения API.

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

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

Это улучшило диагностику, но связало структуру цепочки ошибок с поведением публичного API: изменения wrapping теперь могут менять результаты проверок клиентского кода.

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

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

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

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

errors.Is при обходе ошибки проверяет саму ошибку, а затем переходит к причине, возвращённой методом Unwrap() error. Если тип реализует Unwrap() []error, обход выполняется по нескольким причинам.

Следовательно, добавление Unwrap меняет не только внутреннюю реализацию. Например, проверка errors.Is(err, ErrQuota) могла быть ложной до изменения и стать истинной после него. Аналогично errors.As может начать находить тип, который раньше был недоступен.

При проектировании публичного API нужно добавлять Unwrap, если раскрытие причины является намеренным контрактом. Если библиотека хочет сохранить свободу менять внутренние ошибки, не следует без необходимости предоставлять доступ к конкретной причине; вместо этого можно определить стабильное поведение через собственный тип или метод Is.

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

package main import ( "errors" "fmt" ) var ErrQuota = errors.New("quota exceeded") type quotaError struct{ cause error } func (e quotaError) Error() string { return "request rejected" } func (e quotaError) Unwrap() error { return e.cause } func main() { err := quotaError{cause: ErrQuota} fmt.Println(errors.Is(err, ErrQuota)) // true }

Без Unwrap значение ErrQuota оставалось бы скрытым, и такая проверка вернула бы false. Сам текст Error() здесь не влияет на результат.

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

Публичный HTTP-клиент возвращал тип requestError, внутри которого хранилась ошибка транспорта. Изначально транспортная ошибка была полностью внутренней, поэтому библиотека могла менять реализацию без обещаний клиенту.

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

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

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

  1. Всегда ли добавление Unwrap считается безопасным расширением API?

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

  1. Становится ли весь текст или внутреннее состояние ошибки публичным после добавления Unwrap?

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

  1. Можно ли заменить Unwrap методом Is, чтобы сохранить совместимость?

Иногда да. Метод Is позволяет объявить стабильное соответствие категории без раскрытия конкретной внутренней ошибки. Однако он должен описывать намеренный контракт и не обязан предоставлять полный доступ к причине; если клиенту нужны данные конкретного типа, потребуется Unwrap или другой публичный API.