Files
dev-conventions/conventions/lang/go/db-identifiers.md
T
av 59a1c23f55 язык: заведён блок ПРИМЕРЫ
- пятая, необязательная часть правила: код парой «плохо → хорошо» после
  обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии
  языка во всех тринадцати файлах
- сказано, чем примеры не являются: требований в блоке нет, дословным
  сниппетом он не служит, при расхождении с нормой правят пример
- READING.md обновлён по META-30, в машинные проверки добавлен порядок
  блоков, в читательские — что примеры норму не расширяют
2026-07-26 16:09:58 +03:00

9.6 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 её искал. Сфабрикованный транспортом, он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.