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

Как выбрать между sentinel ошибкой и типизированной ошибкой при проектировании публичного API Go?

Как выбрать между sentinel-ошибкой и типизированной ошибкой при проектировании публичного API Go?

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

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

Sentinel-ошибку выбирают для небольшой стабильной категории, которую вызывающий проверяет по идентичности через errors.Is. Типизированную ошибку выбирают, когда вместе с категорией нужно передать структурированные данные, доступные через errors.As. Выбор определяет публичный контракт пакета: sentinel фиксирует значение ошибки, а типизированная ошибка — её тип и доступный интерфейс.

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

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

С развитием стандартных средств обработки ошибок появились обёртки и функции errors.Is и errors.As. Это позволило отделить стабильную семантику ошибки от её текстового представления и проектировать ошибки как часть API.

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

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

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

Сравнение текстов ошибки ненадёжно: сообщение можно изменить ради читаемости, локализации или добавления контекста без изменения причины сбоя.

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

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

Используйте типизированную ошибку, когда вызывающему нужны данные для решения: имя ресурса, допустимый размер, код ответа, диапазон значений или другая структурированная информация. Доступ к данным должен идти через стабильные методы или экспортируемые поля, а не через хрупкий разбор строки Error().

Минимальная иллюстрация типизированного варианта:

package quota import "fmt" type Error struct { Used, Limit int } func (e *Error) Error() string { return fmt.Sprintf("quota exceeded: %d/%d", e.Used, e.Limit) }

Здесь вызывающий может распознать ошибку через errors.As и получить Used и Limit. Но имя типа quota.Error, его форма и публичные поля становятся частью API, поэтому для долгоживущей библиотеки часто безопаснее предоставлять методы вроде Used() и Limit().

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

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

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

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

Разумное решение — публичная типизированная ошибка с небольшим стабильным набором методов: категорией отказа, кодом банка и идентификатором операции. Клиент принимает решение по структурированным данным, а пакет сохраняет свободу менять человекочитаемый текст. Результат — предсказуемая обработка ошибок без зависимости от сообщения и без передачи служебных данных отдельными каналами.

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

1. Можно ли считать типизированную ошибку полностью внутренней деталью, если она возвращается через error?

Нет. Сам факт, что вызывающий может надёжно получить этот тип через errors.As, делает его наблюдаемой частью поведения пакета. Даже если конкретная структура не документирована, клиенты могут начать от неё зависеть, поэтому тип и его методы нужно проектировать с учётом обратной совместимости.

2. Почему поля типизированной ошибки не всегда стоит делать экспортируемыми?

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

3. Когда sentinel-ошибка становится слишком грубым контрактом?

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