Files
dev-conventions/lang/go/time.md
T
av 4a59c71737 заведён канон общих конвенций для личных проектов
- 13 конвенций по осям arch / lang / stack / common; репозитории берут
  оттуда копии в свой docs/conventions/ и коммитят их у себя
- conv — синхронизация копий: add / status / diff / pull / push, локальные
  регионы исключены из сравнения, поэтому расхождение не даёт шума
2026-07-25 18:18:18 +03:00

4.1 KiB
Raw Blame History

status, extends
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:

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; календарные вычисления бизнес-логики берут зону явно, как описано в базе.