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

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

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

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

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

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

Оборачивание через %w допустимо только тогда, когда вложенная причина также сознательно становится наблюдаемой частью контракта. Иначе используется %v либо формируется новая публичная ошибка без сохранения внутренней цепочки.

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

В Go ошибки изначально были обычными значениями, возвращаемыми явно. Такой подход позволил отделить штатное управление ошибками от исключений, но оставил разработчику задачу определить, какие свойства ошибки безопасно использовать вызывающему коду.

С появлением стандартной поддержки wrapping в Go 1.13 появилась возможность сохранять причинную цепочку. Это решило проблему потери контекста, но одновременно сделало цепочку частью наблюдаемого поведения: сохранённая через %w причина может влиять на решения клиентского кода.

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

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

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

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

Сначала определяют семантические состояния, которые действительно нужны клиенту: ресурс не найден, запрос отклонён из-за конфликта, операция временно недоступна. Затем для таких состояний создают стабильный публичный контракт — обычно экспортируемый sentinel или экспортируемый тип с гарантированными полями.

Внутреннюю причину можно обернуть в публичную ошибку, если клиенту полезно сохранить контекст и допустимо раскрыть факт существования этой категории. Но нельзя механически передавать наружу любую внутреннюю ошибку через %w: это делает конкретный тип или sentinel частью API, даже если разработчик этого не планировал.

package storage import "fmt" var ErrNotFound = fmt.Errorf("resource not found") func load(id string) error { return fmt.Errorf("load %q: %w", id, ErrNotFound) }

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

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

Компромисс заключается между диагностикой и стабильностью API. Чем больше причин сохраняется в цепочке, тем больше информации получает клиент, но тем сильнее он зависит от внутренней реализации и тем сложнее менять библиотеку без обратной несовместимости.

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

Команда разрабатывает клиентскую библиотеку для хранилища. В первой версии она оборачивает ошибки драйвера через %w, чтобы пользователи получали подробную диагностику. Несколько клиентов начинают принимать решения по типам драйвера: один повторяет операцию при сетевой ошибке, другой считает конкретную ошибку драйвера признаком отсутствия объекта.

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

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

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

  1. Вопрос: Делает ли добавление %w ошибку автоматически частью публичного контракта?

    Ответ: Да, если возвращаемая ошибка доступна внешнему клиенту. Сохранив вложенную причину, пакет позволяет клиенту обнаружить её по цепочке и начать на неё опираться. Поэтому %w следует рассматривать не только как способ улучшить текст диагностики, но и как решение о раскрытии API.

  2. Вопрос: Когда публичный тип ошибки предпочтительнее sentinel-объекта?

    Ответ: Тип нужен, когда кроме самой категории необходимо передавать структурированные данные: имя ресурса, код отказа, допустимый предел или направление повтора. Sentinel проще для конечного фиксированного набора состояний, но не должен превращаться в контейнер для данных через разбор текста.

  3. Вопрос: Можно ли считать текст ошибки стабильным контрактом, если он хорошо документирован?

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