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

72 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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; календарные вычисления бизнес-логики берут зону
явно, как описано в базе.
<!-- local:механизировано -->
<!-- /local -->