- тема — набор правил об одном фокусе разработки, имя латиницей (нижний 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
130 lines
9.5 KiB
Markdown
130 lines
9.5 KiB
Markdown
---
|
|
topic: db-identifiers
|
|
prefix: GKEY
|
|
extends: 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 её искал. Сфабрикованный транспортом,
|
|
он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли
|
|
вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.
|