6.6 KiB
6.6 KiB
Ошибки
Конвенция: как устроены и передаются ошибки в jellybit. Правила оформления кода (How). Где и когда ошибку логировать — в logging.md, раздел «Ошибки» (коротко: лог один раз на доменной границе). Здесь — как ошибки строятся, оборачиваются и проверяются.
Базовая идиома: stdlib
- Только стандартный
errors+fmt.Errorf. Безpkg/errors(в режиме поддержки) иcockroachdb/errors(стек-трейсы/Sentry — избыточно для домашнего сервиса). Контекст ошибки несётslog, а не стек. - Если отладка начнёт упираться в «где именно родилась ошибка» — это сигнал пересмотреть, а не дефолт.
Обёртка и контекст
jellybit — приложение, а не библиотека: внешнего Go-API нет, весь код
наш. Поэтому внутри приложения обёртка %w — дефолт, чтобы errors.Is/
errors.As работали сквозь слои.
- Добавляем контекст обёрткой:
fmt.Errorf("parse magnet: %w", err). %w— когда вызывающий может инспектировать причину (наш обычный случай).%v— когда причину сознательно не раскрываем (не хотим завязывать вызывающего на чужой тип ошибки).- От утечки внутренних ошибок наружу защищаемся не через
%vв цепочке, а трансляцией на внешней границе (см. ниже).
Стиль сообщения:
- со строчной, без точки в конце, без «failed to»/«error» — обёртка и так читается как «контекст: причина»;
- контекст — операция/субъект:
"link target: %w", не"something failed"; - без заикания: каждый слой добавляет свой смысл, не повторяет нижний
(
"add to qbt: %w", а не"add download failed: add to qbt failed: …").
Проверка ошибок
- Сравнение — только
errors.Is(err, ErrX)(неerr == ErrX) иerrors.As(err, &target). Никогда не матчим по тексту (strings.Contains(err.Error(), …)). - Граничные ошибки зависимостей транслируем в доменные у источника:
sql.ErrNoRows→ доменныйstore.ErrNotFoundв слое store, чтобы выше по коду не торчалdatabase/sql.
Sentinel vs типизированные
- Sentinel (
var ErrNotFound = errors.New("not found")) — для условий, на которые ветвится код (нет записи, дубликат по infohash, неподдерживаемый источник). Проверяемerrors.Is. - Типизированная ошибка (тип с полями + метод
Error()) — когда вызывающему нужны данные ошибки (поле валидации, код). Достаёмerrors.As. Не плодим типы там, где хватает sentinel.
Граница и трансляция: приватный vs публичный канал
Внутри — богатые обёрнутые ошибки. На внешней границе ошибку транслируем, и форма зависит от канала, кто его видит:
- Приватный канал — логи (владелец сервиса). Полная ошибка со всей
цепочкой
%wи контекстом. Пишется один раз на доменной границе — см. logging.md. - Публичный канал — пользовательские поверхности (Telegram, web-UI, HTTP
API; ими пользуется не только владелец). Сюда отдаём:
- человекочитаемое сообщение по доменной ошибке — не сырой
err.Error()и не детали реализации (database/sql, пути, стек); - + корреляционный ключ для владельца —
download_id(если операция к загрузке) либоrequest_id, чтобы по нему найти полную ошибку в логах. Пример: «При обработке загрузки произошла ошибка, download_id=12345», а не «произошла ошибка» и не сырой текст; - маппинг доменной ошибки → статус/сообщение:
ErrNotFound→ 404 «не найдено», валидация/ErrNotMagnet→ 400 «некорректный источник», конфликт состояния (ErrConflict— операция недопустима в текущем состоянии) → 409 «действие недоступно в текущем состоянии», прочее → 500 «внутренняя ошибка».
- человекочитаемое сообщение по доменной ошибке — не сырой
Граница публичная по умолчанию. Истинно приватный для владельца канал — логи; отдельной «операторской» поверхности с сырыми ошибками не заводим.
panic
panic— только для невосстановимого: баг программиста (нарушенный инвариант), ошибка инициализации, из которой нельзя стартовать.- Не для управления потоком и не для ожидаемых ошибок (нет сети, плохой
ввод) — это значения
error. recover— на верхней границе обработчика (HTTP middleware), чтобы один паникующий запрос не ронял процесс.
Несколько ошибок
- Сбор независимых ошибок (напр. валидация конфига — все проблемы разом) —
errors.Join; проверка собранного по-прежнему черезerrors.Is.