--- status: рекомендуемая extends: arch/time.md --- # Время: реализация на Go ## Единая точка - «Сейчас» берём у слоя хранилища — `store.Now()`, а не `time.Now()` по коду. Ценность точки — **гарантированный UTC и один формат**: `Now()` возвращает `time.Now().UTC()`, и ни одна ветка кода не может об этом забыть. Побочно это единственное место, которое придётся превратить в переменную или поле, если однажды понадобится подменять часы в тестах, — но само по себе оно тестируемости не даёт. - Форматирование и разбор — `store.FormatTime` / `store.ParseTime` поверх `time.RFC3339`. - Запрет прямого `time.Now()` механизируется `forbidigo`. Исключений ровно два, и оба обязаны быть прописаны, иначе конвенция противоречит сама себе: сама точка `Now()` и обёртка измерения длительности (ниже). ## Точность и разбор - В БД — **секундная точность**, ширина 20 символов (`2026-06-28T11:23:45Z`). Она получается сама: layout `time.RFC3339` не содержит долей секунды, поэтому `Format` их не выведет. - `time.RFC3339Nano` не используем: он отбрасывает хвостовые нули и ломает фиксированную ширину. - `time.Parse(time.RFC3339, …)` принимает и доли, и не-`Z` офсеты, то есть канонический вид гарантирует **писатель**, а не читатель. Для одного писателя этого достаточно; чужой вход нормализуем явно. - В драйвер отдаём строку из `FormatTime`, а не `time.Time`: колонка — `TEXT`, и промежуточное преобразование драйвером нам не нужно. ## Логи `slog` по умолчанию **не даёт UTC**: встроенные хендлеры пишут время в зоне самого `time.Time`, то есть в локальной зоне процесса, — на ноутбуке разработчика логи молча поедут в `+03:00`. UTC ставится `ReplaceAttr` по `slog.TimeKey`: ```go func utcTime(_ []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { a.Value = slog.TimeValue(a.Value.Time().UTC()) } return a } ``` `JSONHandler` пишет миллисекунды — три знака, фиксированная ширина. Это другая точность, чем в БД, и это нормально: ширина фиксируется на носитель (см. базу). ## Длительность Обёртка измерения — **легитимное исключение из запрета `time.Now()`**, и без него не обойтись: `store.Now()` приводит время к UTC через `.UTC()`, а это **срезает монотонную составляющую** `time.Time`. Интервал, посчитанный по таким меткам, зависит от подводки часов. Поэтому обёртка берёт `time.Now()` напрямую и считает `time.Since` — с локальным `//nolint`. ## Зоны `time/tzdata` импортируется в `main`, зона отображения валидируется загрузчиком конфига — см. `lang/go/config.md`. Применяется она только в шаблонах и форматтерах UI; календарные вычисления бизнес-логики берут зону явно, как описано в базе.