ПОЧЕМУ стало ключевым словом, язык поднят до версии 2
- метка обоснования пишется заглавными и вошла в словарь набора: скелет правила теперь целиком из ключевых слов, а не смесь `**ДОЛЖЕН.**` и `**Почему.**`; в переводе на другой язык метка меняется как остальные слова (ПОЧЕМУ / WHY), 235 вхождений заменены - метки правила выделены из шкалы в отдельный перечень: ПОЧЕМУ и МЕХАНИЗИРОВАНО обязательности не задают, а размечают части, и стандартом не даются ни в одном языке — раньше МЕХАНИЗИРОВАНО висело строкой в таблице модальности - версия языка поднята до 2, потому что изменение формы меняет чтение уже написанного текста; строка о версии в двенадцати конвенциях перечисляет теперь и метки, а служебные слова сценария в неё по-прежнему не входят
This commit is contained in:
@@ -8,9 +8,9 @@ prefix: GERR
|
||||
**логировать** — в конвенции `logging` (коротко: лог один раз на доменной
|
||||
границе).
|
||||
|
||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
||||
только тогда, когда написаны заглавными.
|
||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2 —
|
||||
тогда и только тогда, когда написаны заглавными.
|
||||
|
||||
Две границы, о которых говорят правила ниже:
|
||||
|
||||
@@ -30,7 +30,7 @@ prefix: GERR
|
||||
**ДОЛЖЕН.** Ошибки создаются и оборачиваются через `errors` и
|
||||
`fmt.Errorf`; библиотеки со стек-трейсами не подключаются.
|
||||
|
||||
**Почему.** Стек и цепочка обёрток решают одну задачу — локализацию места.
|
||||
**ПОЧЕМУ.** Стек и цепочка обёрток решают одну задачу — локализацию места.
|
||||
При дисциплине «каждый слой добавляет свой контекст» (GERR-3) цепочка сообщений
|
||||
локализует не хуже, а стоит ничего: остальной контекст ошибки уже несёт
|
||||
`slog`. Библиотека со стеками добавляет зависимость, собственный тип ошибки
|
||||
@@ -45,7 +45,7 @@ prefix: GERR
|
||||
**НЕ ДОЛЖЕН.** Пакет со стек-трейсами не заводится в отдельном месте
|
||||
кодовой базы ради конкретной отладки.
|
||||
|
||||
**Почему.** В коде появляются два способа устроить ошибку, и вызывающий
|
||||
**ПОЧЕМУ.** В коде появляются два способа устроить ошибку, и вызывающий
|
||||
перестаёт знать, какой перед ним: обёртки склеиваются по-разному,
|
||||
`errors.Is` работает не везде одинаково. Хуже второе: боль, снятая
|
||||
локально, перестаёт накапливаться — а накопление и есть единственный
|
||||
@@ -56,7 +56,7 @@ prefix: GERR
|
||||
**ДОЛЖЕН.** Ошибка, возвращаемая на уровень выше, оборачивается с
|
||||
контекстом: `fmt.Errorf("parse magnet: %w", err)`.
|
||||
|
||||
**Почему.** На этом держится GERR-1: цепочка заменяет стек ровно настолько,
|
||||
**ПОЧЕМУ.** На этом держится GERR-1: цепочка заменяет стек ровно настолько,
|
||||
насколько слои в неё пишут. Слой, пробросивший ошибку без своего контекста,
|
||||
стирает участок пути — по итоговому сообщению нельзя сказать, через какую
|
||||
операцию ошибка прошла, и отладка «no such file» начинается с чтения всего
|
||||
@@ -72,7 +72,7 @@ prefix: GERR
|
||||
| GERR-4.1 | вызывающий может инспектировать причину (обычный случай) | `%w` |
|
||||
| GERR-4.2 | причину сознательно не раскрываем | `%v` |
|
||||
|
||||
**Почему.** Возражение против дефолтного `%w` — «обёрнутая ошибка
|
||||
**ПОЧЕМУ.** Возражение против дефолтного `%w` — «обёрнутая ошибка
|
||||
становится частью API» — относится к библиотекам с внешними потребителями.
|
||||
Сервис же — приложение: внешнего Go-API нет, весь код наш, контракт
|
||||
меняется вместе с вызывающими. Зато `%v` в середине цепочки обрывает
|
||||
@@ -86,7 +86,7 @@ prefix: GERR
|
||||
**НЕ ДОЛЖЕН.** `%v` не используется как средство не пустить внутреннюю
|
||||
ошибку наружу.
|
||||
|
||||
**Почему.** Обрыв цепочки внутри кода не мешает тексту уехать наружу
|
||||
**ПОЧЕМУ.** Обрыв цепочки внутри кода не мешает тексту уехать наружу
|
||||
целиком: наружу отдаёт внешняя граница, и если она отдаёт `err.Error()`,
|
||||
детали утекут при любом глаголе. Подмена не решает задачу, ради которой
|
||||
сделана, а плату берёт сразу — `errors.Is` ломается у всех вызывающих.
|
||||
@@ -96,7 +96,7 @@ prefix: GERR
|
||||
|
||||
**СЛЕДУЕТ.** Без точки в конце, без «failed to» и «error».
|
||||
|
||||
**Почему.** Цепочка склеивается в одну строку через `": "`, и обёртка
|
||||
**ПОЧЕМУ.** Цепочка склеивается в одну строку через `": "`, и обёртка
|
||||
читается как «контекст: причина» — заглавные буквы и точки рвут эту строку
|
||||
на середине. Слова «failed» и «error» не несут информации: то, что перед
|
||||
нами ошибка, известно из того, что это ошибка. Зато повторяются они на
|
||||
@@ -106,7 +106,7 @@ prefix: GERR
|
||||
|
||||
**СЛЕДУЕТ.** В обёртку идёт то, что делал слой: `"link target: %w"`.
|
||||
|
||||
**Почему.** Обёртка ценна ровно тем, что сужает место (GERR-3). «something
|
||||
**ПОЧЕМУ.** Обёртка ценна ровно тем, что сужает место (GERR-3). «something
|
||||
failed» не сужает ничего и при этом занимает в сообщении место, которое мог
|
||||
бы занять единственный полезный здесь факт — имя операции.
|
||||
|
||||
@@ -115,7 +115,7 @@ failed» не сужает ничего и при этом занимает в
|
||||
**НЕ СЛЕДУЕТ.** Обёртка не пересказывает то, что уже сказал уровень ниже:
|
||||
`"add to qbt: %w"`, а не `"add download failed: add to qbt failed: …"`.
|
||||
|
||||
**Почему.** Повтор удлиняет сообщение, не добавляя локализации: одно и то
|
||||
**ПОЧЕМУ.** Повтор удлиняет сообщение, не добавляя локализации: одно и то
|
||||
же событие названо дважды. Читателю приходится проверять, не два ли это
|
||||
разных места в коде, — то есть заикание не просто бесполезно, оно стоит
|
||||
времени при каждом чтении лога.
|
||||
@@ -132,7 +132,7 @@ failed» не сужает ничего и при этом занимает в
|
||||
возникла: `sql.ErrNoRows` → `store.ErrNotFound` в слое store; то же для
|
||||
HTTP-клиентов, файловой системы, внешних SDK.
|
||||
|
||||
**Почему.** Иначе тип зависимости становится частью контракта всех слоёв
|
||||
**ПОЧЕМУ.** Иначе тип зависимости становится частью контракта всех слоёв
|
||||
выше: чтобы отличить «нет записи», доменный код импортирует `database/sql`
|
||||
и сравнивает с его sentinel'ом. Замена хранилища или SDK правит тогда не
|
||||
адаптер, а все ветвления в приложении — притом что снаружи адаптера
|
||||
@@ -148,7 +148,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
| GERR-10.1 | ветвление по условию: нет записи, дубликат, неподдерживаемый источник | sentinel `var ErrNotFound = errors.New("not found")`, проверка `errors.Is` |
|
||||
| GERR-10.2 | данные ошибки: поле валидации, код, лимит | тип с полями и методом `Error()`, извлечение `errors.As` |
|
||||
|
||||
**Почему.** Sentinel — одно значение; сравнение с ним не зависит от
|
||||
**ПОЧЕМУ.** Sentinel — одно значение; сравнение с ним не зависит от
|
||||
структуры ошибки и переживает добавление полей. Тип заводится ради данных,
|
||||
и тип без данных отвечает вызывающему ровно то же, что sentinel, но ценой
|
||||
объявления, `errors.As` и вопроса «сравнивать по типу или по значению» на
|
||||
@@ -159,7 +159,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
|
||||
**НЕ ДОЛЖЕН.** Ветвление по содержимому `err.Error()` не используется.
|
||||
|
||||
**Почему.** Текст сообщения — не контракт: GERR-6–GERR-8 разрешают
|
||||
**ПОЧЕМУ.** Текст сообщения — не контракт: GERR-6–GERR-8 разрешают
|
||||
переписывать его свободно. Правка формулировки в нижнем слое молча ломает
|
||||
ветвление наверху, и компилятор этого не видит. Это то же самое, что
|
||||
публичный API из строки лога.
|
||||
@@ -175,7 +175,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**ДОЛЖЕН.** В лог уходит вся цепочка `%w` с контекстом; где и когда именно
|
||||
— конвенция `logging`.
|
||||
|
||||
**Почему.** Цепочка — единственный носитель диагностики (GERR-1), и
|
||||
**ПОЧЕМУ.** Цепочка — единственный носитель диагностики (GERR-1), и
|
||||
единственный канал, где её можно показать целиком, — тот, который видит
|
||||
владелец. Не записанная там, она не сохранится нигде: наружу идёт
|
||||
нейтральное сообщение (GERR-13), и восстанавливать причину будет не из чего.
|
||||
@@ -185,7 +185,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**ДОЛЖЕН.** Наружу идёт человекочитаемый текст по доменной ошибке, а не
|
||||
`err.Error()` и не детали реализации (`database/sql`, пути, стек).
|
||||
|
||||
**Почему.** Внутренние детали пользователю нечитаемы, а владельцу не нужны
|
||||
**ПОЧЕМУ.** Внутренние детали пользователю нечитаемы, а владельцу не нужны
|
||||
— у него есть лог (GERR-12). Зато они раскрывают устройство системы — имена
|
||||
таблиц, пути на диске, версии зависимостей — тому, кто их знать не должен,
|
||||
причём раскрывают именно в момент, когда что-то пошло не так.
|
||||
@@ -196,7 +196,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
«При обработке загрузки произошла ошибка, download_id=…» вместо «произошла
|
||||
ошибка».
|
||||
|
||||
**Почему.** GERR-13 забирает у пользователя всю фактуру; без ключа его
|
||||
**ПОЧЕМУ.** GERR-13 забирает у пользователя всю фактуру; без ключа его
|
||||
обращение звучит как «у меня что-то не работает», и владелец ищет запись в
|
||||
логе по времени и догадкам. Ключ соединяет нейтральный ответ с полной
|
||||
ошибкой в логе, не раскрывая наружу ничего сверх того, что пользователь уже
|
||||
@@ -208,7 +208,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
задаётся один раз; транспорт без статусов (бот) берёт из него только
|
||||
сообщение.
|
||||
|
||||
**Почему.** Иначе одна и та же ошибка отвечает по-разному в HTTP и в боте,
|
||||
**ПОЧЕМУ.** Иначе одна и та же ошибка отвечает по-разному в HTTP и в боте,
|
||||
и расхождение обнаруживается не как дефект, а как жалоба. Вторая причина
|
||||
важнее: единственная точка — это место, куда механически дописывается новая
|
||||
ветвь (GERR-16). Маппинг, размазанный по хендлерам, требование «дописать везде»
|
||||
@@ -219,7 +219,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**ДОЛЖЕН.** Ожидаемый отказ (конфликт, валидация) заводится sentinel'ом и
|
||||
добавляется в маппинг (GERR-15) тем же изменением.
|
||||
|
||||
**Почему.** Ветка `default` врёт в обе стороны: транспорт отдаёт 500
|
||||
**ПОЧЕМУ.** Ветка `default` врёт в обе стороны: транспорт отдаёт 500
|
||||
«внутренняя ошибка» на нормальный конфликт, а логирующая граница списывает
|
||||
его в `ERROR` вместо `DEBUG`. Второе хуже первого — штатные отказы начинают
|
||||
шуметь в логе ровно там, где по нему ищут настоящие поломки.
|
||||
@@ -230,7 +230,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
наружу 500 и нейтральное «внутренняя ошибка», а в лог идёт `ERROR` с
|
||||
признаком того, что маппинг её не знает.
|
||||
|
||||
**Почему.** Непокрытая ошибка — не класс отказа, а дефект: ветвь забыли
|
||||
**ПОЧЕМУ.** Непокрытая ошибка — не класс отказа, а дефект: ветвь забыли
|
||||
завести вопреки GERR-16. Адресат у неё владелец в смысле «надо чинить», отсюда
|
||||
`ERROR` — уровень выбирается по адресату (конвенция `logging`). Статус
|
||||
тоже не выбирается: известное пользовательское состояние лежало бы в
|
||||
@@ -256,7 +256,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
Появился второй зритель или публичный доступ к экрану состояния —
|
||||
поверхность стала публичным каналом, и на неё распространяется GERR-17.1.
|
||||
|
||||
**Почему.** Транзиентный ответ читает тот, кто нажал кнопку: сырой текст
|
||||
**ПОЧЕМУ.** Транзиентный ответ читает тот, кто нажал кнопку: сырой текст
|
||||
ему ничего не объясняет, а владельцу не нужен — у него лог. Персистентную
|
||||
диагностику читает владелец, и она отвечает на вопрос «почему сломалась вот
|
||||
эта запись» через месяц, когда лог уже ротировался; нейтральное «произошла
|
||||
@@ -269,7 +269,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**НЕ ДОЛЖЕН.** Токены, пароли и ключи не попадают ни в транзиентный ответ,
|
||||
ни в персистентную диагностику; источник вычищается на границе клиента.
|
||||
|
||||
**Почему.** Запрет абсолютен, потому что персистентная диагностика живёт в
|
||||
**ПОЧЕМУ.** Запрет абсолютен, потому что персистентная диагностика живёт в
|
||||
БД: уезжает в бэкапы, попадает в скриншоты и выгрузки и переживает ротацию
|
||||
самого секрета. Вычистка на границе клиента — единственное место, где ещё
|
||||
известно, какие поля запроса секретны: дальше ошибка едет как текст, и
|
||||
@@ -280,7 +280,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**ДОЛЖЕН.** Персистентная диагностика не кладётся в доменное поле, которое
|
||||
показывают пользователю.
|
||||
|
||||
**Почему.** Различие GERR-17.1 и GERR-17.2 держится на том, что у поверхностей
|
||||
**ПОЧЕМУ.** Различие GERR-17.1 и GERR-17.2 держится на том, что у поверхностей
|
||||
разные поля. Одно поле на оба назначения означает, что при первом же показе
|
||||
записи наружу сырой текст уедет туда же — не по решению, а потому что поле
|
||||
одно.
|
||||
@@ -292,7 +292,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**ДОЛЖЕН.** Паникой отмечается нарушенный инвариант (баг программиста) и
|
||||
ошибка инициализации, из которой нельзя стартовать.
|
||||
|
||||
**Почему.** Паника не оставляет вызывающему выбора: обработать её на месте
|
||||
**ПОЧЕМУ.** Паника не оставляет вызывающему выбора: обработать её на месте
|
||||
нельзя, можно только уронить единицу обработки. Это верный ответ, когда
|
||||
состояние процесса перестало описываться кодом: работа с нарушенным
|
||||
инвариантом опаснее падения, а сервис, стартовавший без обязательной
|
||||
@@ -303,7 +303,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**НЕ ДОЛЖЕН.** Паника не используется для управления потоком: нет сети,
|
||||
плохой ввод, отсутствующая запись возвращаются как `error`.
|
||||
|
||||
**Почему.** Сигнатура — единственное, что сообщает вызывающему о возможном
|
||||
**ПОЧЕМУ.** Сигнатура — единственное, что сообщает вызывающему о возможном
|
||||
отказе; отказ, брошенный паникой, из неё не виден, и компилятор не заставит
|
||||
его обработать. Дальше такая паника долетает до recover-границы (GERR-22), где
|
||||
неотличима от бага: штатный отказ попадает в лог со стеком и с интонацией
|
||||
@@ -318,7 +318,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
| GERR-22.1 | HTTP-хендлер | `net/http` восстанавливает панику сам и процесс не роняет; свой `recover` нужен, чтобы отдать контролируемый 500 и записать событие в `slog`, а не в stdlib-логгер |
|
||||
| GERR-22.2 | цикл обработки апдейтов бота, фоновый воркер | паника в горутине роняет процесс; `recover` ставится в той же горутине |
|
||||
|
||||
**Почему.** `recover` работает только в той горутине, где случилась паника,
|
||||
**ПОЧЕМУ.** `recover` работает только в той горутине, где случилась паника,
|
||||
поэтому «у нас есть recover в HTTP» не защищает воркер — граница нужна у
|
||||
каждой единицы отдельно. Без неё один плохой апдейт бота или одна запись с
|
||||
неожиданным полем гасят весь сервис, включая части, к этой ошибке
|
||||
@@ -330,7 +330,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
|
||||
**ДОЛЖЕН.** Логирующий `recover` кладёт в запись стек.
|
||||
|
||||
**Почему.** Это единственное место, где стек нужен (GERR-1): у восстановленной
|
||||
**ПОЧЕМУ.** Это единственное место, где стек нужен (GERR-1): у восстановленной
|
||||
паники цепочки `%w` нет вовсе. «index out of range» без стека не
|
||||
диагностируется в принципе — сообщение не называет ни файла, ни операции,
|
||||
по нему нельзя сказать даже, в каком пакете упало.
|
||||
@@ -347,7 +347,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
| GERR-26.1 | обработчик HTTP-запроса | ответ 500, если он ещё не начат; процесс и прочие запросы не затрагиваются |
|
||||
| GERR-26.2 | итерация цикла обработки — бот, воркер | цикл продолжается со следующего элемента, упавший элемент повторно не берётся |
|
||||
|
||||
**Почему.** Паника внутри обработки одного элемента почти всегда говорит о
|
||||
**ПОЧЕМУ.** Паника внутри обработки одного элемента почти всегда говорит о
|
||||
баге в работе с данными этого элемента, а не о порче общего состояния, —
|
||||
останавливать всё остальное не за что. Довод «let it crash» здесь работает
|
||||
не буквально: в OTP падает изолированный процесс под супервизором, а не узел
|
||||
@@ -378,7 +378,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
||||
**СЛЕДУЕТ.** Валидация конфига и подобные проверки отдают все проблемы
|
||||
разом; проверка собранного — по-прежнему через `errors.Is`.
|
||||
|
||||
**Почему.** Возврат первой ошибки превращает починку конфига в серию
|
||||
**ПОЧЕМУ.** Возврат первой ошибки превращает починку конфига в серию
|
||||
перезапусков, по одной проблеме за прогон. Склейка сообщений в строку даёт
|
||||
тот же список, но убивает ветвление: `errors.Is` по такому результату не
|
||||
находит ничего, и вызывающий остаётся с текстом, матчить который запрещено
|
||||
|
||||
Reference in New Issue
Block a user