Время в Go API: RFC3339, смещение и нормализация UTC
Партнёр присылает время события строкой. Один вызов содержит 2026-01-02T10:00:00+03:00, другой — 2026-01-02T07:00:00Z. Если использовать строку как ключ или сравнивать её лексикографически, одинаковый момент выглядит разным. Если принять строку без зоны и подставить настройки машины, результат начнёт зависеть от окружения сервиса.
Договориться о моменте времени
Для поля, означающего точный момент, полезен контракт RFC3339 с обязательным смещением или Z. time.Parse с layout time.RFC3339 проверяет структуру и получает time.Time. Метод UTC меняет представление зоны, сохраняя момент. После нормализации можно одинаково сериализовать время для журнала, сообщения и записи в хранилище.
Go использует особое правило layout: формат задаётся примером эталонной даты, а не буквами YYYY-MM-DD. Готовая константа RFC3339 позволяет не собирать его вручную и уменьшает риск перепутать месяц с минутами. Если поле по контракту является датой без времени, ему нужен другой формат и другая семантика; добавлять полночь UTC автоматически не следует.
В примере парсер возвращает нормализованный момент. Два разных смещения сравниваются через Equal, затем форматируются в один вид. Строка без зоны отвергается. Проверки выражают требование к границе сервиса, а не только способность библиотеки разобрать один красивый пример.
Разобрать и сравнить моменты
package main
import (
"fmt"
"time"
)
func parseMoment(raw string) (time.Time, error) {
value, err := time.Parse(time.RFC3339, raw)
if err != nil {
return time.Time{}, fmt.Errorf("event time: %w", err)
}
return value.UTC(), nil
}
func main() {
a, err := parseMoment("2026-01-02T10:00:00+03:00")
if err != nil {
panic(err)
}
b, err := parseMoment("2026-01-02T07:00:00Z")
if err != nil {
panic(err)
}
if !a.Equal(b) || a.Format(time.RFC3339) != "2026-01-02T07:00:00Z" {
panic("moments differ")
}
if _, err := parseMoment("2026-01-02T10:00:00"); err == nil {
panic("missing zone accepted")
}
fmt.Println(a.Format(time.RFC3339), a.Equal(b))
}
Выполните go run main.go. Вывод содержит UTC-представление и true для равенства моментов. Для сравнения времени предпочитайте Equal: оператор == также учитывает внутреннее представление time.Time, включая связанные сведения о зоне и монотонных часах. Для ключа события лучше использовать отдельный event_id, поскольку совпадение времени само по себе не означает одно и то же событие.
Зона отчёта и точность времени
Нормализация в UTC удобна для обмена, но пользовательский календарный день определяется зоной предметной области. Для отчёта по местным суткам сначала переведите момент в нужную зону, затем извлеките дату. Хранение только местной строки без смещения потеряет информацию при повторяющемся часе перехода времени. Эти правила должны быть едины у производителя и потребителя сообщения.
Если точность дробной части важна, согласуйте её отдельно. time.Time хранит наносекунды, а разные базы и клиенты могут округлять до микросекунд или миллисекунд. В демонстрации используется секундная точность, чтобы сосредоточиться на смещении. Для контрактного теста возьмите реальное значение с дробью и проверьте весь путь сериализации и чтения, прежде чем использовать его для упорядочивания.
Не исправляйте некорректный вход молча по локальной зоне сервера. Такой fallback делает старые и новые продюсеры неразличимыми: клиент уверен в одном времени, а сервис хранит другое. Если переходный период требует старого формата, оформите его отдельной веткой с явной зоной и наблюдаемым признаком, чтобы затем убрать поддержку.
Проверьте невозможную дату, отсутствующую зону, Z и ненулевое смещение. Отдельно проверьте пограничный переход даты при нормализации: событие сразу после полуночи в +03:00 может оказаться предыдущим днём UTC. Это правильное изменение представления, которое нельзя принимать за потерю исходного события.
Продолжить практику
Следующий шаг — закрепить формат времени в сообщении между независимо обновляемыми сервисами. В курсе микросервисов на Go есть HTTP-контракты, события Kafka и совместимые изменения. Откройте программу и продолжите от парсера одного поля к проверке полного контракта производителя и потребителя.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
