--- prefix: GKEY extends: arch/db-identifiers.md --- # Идентификаторы: реализация на Go Как `arch/db-identifiers.md` выглядит в Go-приложении. Форма записи — `LANGUAGE.md`. Единая точка из `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`), чтобы тут же сопоставить его со своим ответом. **Почему.** Инверсия правила «трансляция у источника» из `lang/go/errors.md`. Sentinel — сообщение от слоя, который знает факт: строка не найдена, потому что store её искал. Сфабрикованный транспортом, он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.