- тема — набор правил об одном фокусе разработки, имя латиницей (нижний kebab-case рекомендуется, годится любой идентификатор, пригодный для имени файла); определение в LANGUAGE.md и README.md - заведены META-28 и META-29: тема объявляется в шапке (`topic:`), стоит в манифесте набора и не переиспользуется; `topic:` добавлен во все 12 файлов - prefixes.toml и topics.toml слиты в manifest.toml — манифест набора против манифеста подключения `.conventions.toml`, разделы topics/prefixes с live и retired
9.5 KiB
topic, prefix, extends
| topic | prefix | extends |
|---|---|---|
| db-identifiers | GKEY | arch/db-identifiers.md |
Идентификаторы: реализация на Go
Как базовый слой выглядит в Go-приложении.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Единая точка из KEYS-3 — пакет internal/ident: он
порождает идентификаторы (NewID, NewIDAt) и он же их разбирает
(Parse). Правила ниже говорят, из каких мест кода эти функции зовутся.
Правила
GKEY-1. Генерация и разбор — только через internal/ident
ДОЛЖЕН. Идентификаторы порождаются и разбираются функциями пакета
internal/ident; других генераторов и парсеров id в коде нет.
ПОЧЕМУ. Реализация KEYS-3 и KEYS-4. Вызов
ULID-библиотеки — одна строка, доступная из любого пакета, и на ревью он не
выглядит нарушением: значение получается валидное, просто мимо нормализации
регистра. Отдельный пакет переводит запрет в проверяемое свойство — импорт
библиотеки где-либо, кроме internal/ident, находится поиском по имени
модуля, а «забытая нормализация» не находится ничем, пока запрос молча не
перестанет находить существующую запись.
GKEY-2. Первичный ключ генерируется в Create-методах store
ДОЛЖЕН. Новая сущность получает значение PK вызовом ident.NewID()
внутри Create-метода слоя store.
ПОЧЕМУ. KEYS-2 требует, чтобы значение было
известно до вставки, но не говорит, кто его присваивает. Store — последний
слой, через который проходят все пути создания строки, включая импорт,
фоновые задания и тесты. Генерация выше по стеку делает присвоение
обязанностью каждого нового вызывающего, и первый забывший запишет пустую
строку в колонку ключа: для строкового PK это валидное значение, база его
не отклонит, и дефект обнаружится на второй такой вставке.
GKEY-3. Прочие идентификаторы генерируются в точке начала операции
ДОЛЖЕН. Идентификатор батча, задания или корреляционный ключ создаётся
вызовом ident.NewID() там, где операция начинается.
ПОЧЕМУ. Смысл такого идентификатора (KEYS-7) —
сшивать записи лога всей операции. Созданный ниже по стеку или в момент
первой записи в базу, он не покрывает начальные шаги — а именно они нужны,
когда операция упала до того, как что-либо записала: без общего ключа эти
записи из лога не собираются вообще.
GKEY-4. Бэкфилл в миграциях — ident.NewIDAt(t)
ДОЛЖЕН. Идентификаторы, проставляемые существующим строкам в Go-миграции, порождаются с историческим временем строки, а не с текущим.
ПОЧЕМУ. Сортировка id тогда сохраняет историческую хронологию, а не
момент прогона миграции. Иначе все затронутые строки получают метку одного
момента, склеиваются в нём и встают в порядке обхода — ORDER BY id
начинает врать ровно на том массиве данных, который старше всего.
Исправить это потом нельзя: исходное время в идентификаторе не
восстановить.
GKEY-5. Разбор — на входных границах, до обращения к store
ДОЛЖЕН. ident.Parse() вызывается в обработчике HTTP-роута, формы или
callback'а бота — раньше, чем идентификатор попадёт в store.
ПОЧЕМУ. Реализация KEYS-5. Граница выбрана
транспортная, потому что только на ней известен источник значения, от
которого зависит реакция (GKEY-8): store видит одинаковую строку независимо от
того, пришла она из URL или из собственной формы, и ответить по-разному
оттуда уже невозможно.
GKEY-6. Id в структурах — обычный string
СЛЕДУЕТ. Поля идентификаторов в доменных и store-структурах имеют тип
string.
ПОЧЕМУ. Отдельный тип окупается только тогда, когда компилятор ловит им
ошибку. От перепутывания двух идентификаторов одной семьи (userID и
authorID) он не спасает — оба будут одного типа, и различают их имена
параметров. Зато он требует конверсий на каждой границе с sql-драйвером,
json и шаблонами, то есть даёт цену без выгоды.
GKEY-7. Отдельный тип — когда появляется вторая семья идентификаторов
ДОПУСКАЕТСЯ. Когда в коде оказываются два вида идентификаторов, которые можно перепутать, для них заводятся различимые типы.
ПОЧЕМУ. Явное разрешение нужно, чтобы GKEY-6 не читался как запрет на
типизацию навсегда. Условие названо ровно то, при котором тип начинает
работать: пока все идентификаторы — string, подстановка одного вида
вместо другого компилируется и обнаруживается только на данных.
GKEY-8. Реакция на невалидный id зависит от источника
ДОЛЖЕН. Когда разбор не удался, ответ определяется тем, откуда пришло значение:
| № | Источник | Ответ |
|---|---|---|
| GKEY-8.1 | путь или query URL | 404 без обращения к store |
| GKEY-8.2 | собственная форма, callback-данные кнопки | 400 либо понятное сообщение («кнопка устарела») |
ПОЧЕМУ. Реализация KEYS-5.1 и KEYS-5.2 в терминах
HTTP-кодов. В случае GKEY-8.1 снаружи это неотличимо от несуществующей записи —
и хорошо: чужая или протухшая ссылка описывается так точно. В случае GKEY-8.2
значение сформировало само приложение, и невалидность означает баг
интерфейса или устаревший экран; ответ «не найдено» здесь выглядит штатно,
в логах не оставляет аномалии и тем самым съедает единственный момент,
когда дефект заметен.
GKEY-9. Транспорт не создаёт доменные ошибки
НЕ ДОЛЖЕН. Обработчик не конструирует доменный sentinel (например
ErrNotFound), чтобы тут же сопоставить его со своим ответом.
ПОЧЕМУ. Инверсия правила «трансляция у источника» из конвенции
errors. Sentinel — сообщение от слоя, который знает факт:
строка не найдена, потому что store её искал. Сфабрикованный транспортом,
он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли
вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.