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

Сервис повторяет операции по тексту ошибки: какое свойство дизайна ошибки позволит заменить такой механизм?

Сервис повторяет операции по тексту ошибки: какое свойство дизайна ошибки позволит заменить такой механизм?

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

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

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

Изначально интерфейс error в Go предоставлял главным образом текстовое описание через метод Error. Этого достаточно для журналирования, но недостаточно для надежной классификации причин без привязки к формулировкам сообщений.

Механизм оборачивания ошибок и функции errors.Is и errors.As позволяют сохранять контекст и извлекать семантические свойства ошибки через цепочку оберток. Это поддерживает разделение между сообщением для человека и контрактом для машинной обработки.

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

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

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

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

Нужно определить отдельную семантическую категорию ошибки, например ErrRetryable, и сделать ее доступной через errors.Is. Конкретная причина при этом должна сохраняться через Unwrap, чтобы диагностика и другие проверки не потеряли исходную ошибку.

package main import ( "errors" "fmt" ) var ErrRetryable = errors.New("операцию можно повторить") type retryableError struct{ cause error } func (e *retryableError) Error() string { return "временная ошибка: " + e.cause.Error() } func (e *retryableError) Unwrap() error { return e.cause } func (e *retryableError) Is(target error) bool { return target == ErrRetryable } func Retryable(cause error) error { return &retryableError{cause: cause} } func main() { err := fmt.Errorf("получение профиля: %w", Retryable(errors.New("соединение закрыто"))) _ = errors.Is(err, ErrRetryable) // true }

Внешний код проверяет errors.Is(err, ErrRetryable), а не текст. Цепочка сохраняет и категорию, и исходную причину: ее можно дополнительно исследовать через errors.Is или errors.As.

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

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

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

Платежный сервис получает временный сбой от провайдера. Первый вариант — искать в сообщении слова вроде «timeout»: он прост, но ломается при изменении текста и не учитывает ошибки, обернутые несколькими слоями. Второй вариант — проверять конкретный тип драйвера: это точнее, но связывает бизнес-слой с реализацией провайдера.

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

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

1. Достаточно ли одного признака «повторяемая ошибка», чтобы безопасно выполнить повтор?

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

2. Следует ли возвращать признак повторяемости из каждой функции слоя приложения?

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

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

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