RFC 3339 в Go: 5 ошибок парсинга дат и как их исправить

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Парсинг RFC 3339 / ISO-8601 в Go: почему даже опытные разработчики спотыкаются

Вы когда-нибудь писали в Go что-то вроде time.Parse("2006-01-02T15:04:05Z", input), получали ошибку, и чувствовали себя так, будто язык нарочно издевается? Знакомо. Парсинг дат и времени — одна из тех тем, где, казалось бы, всё просто, но на практике подстерегает множество ловушек.

На Stack Overflow вопрос "Parsing RFC-3339 / ISO-8601 date-time string in Go" собрал тысячи просмотров. Люди путаются в форматах, забывают про наносекунды, страдают от часовых поясов и злятся на «магическое» число 2006-01-02T15:04:05Z. В этой статье мы разберём 5 самых частых ошибок и покажем, как их избежать раз и навсегда. Поехали!

Немного теории: что такое RFC 3339 и ISO 8601?

Прежде чем нырять в код, давайте быстро разберёмся с терминами. ISO 8601 — это международный стандарт представления даты и времени. RFC 3339 — это его «урезанная» версия, популярная в вебе (например, в JSON, API, логах).

Типичные строки RFC 3339 выглядят так:

2025-01-15T10:30:00Z
2025-01-15T10:30:00+03:00
2025-01-15T10:30:00.123456789Z

В Go для парсинга используется функция time.Parse(layout, value). И вот тут начинается самое интересное.

Ошибка №1: Неправильный layout (шаблон) — главная причина головной боли

Go использует уникальный подход: формат задаётся не кодами типа YYYY-MM-DD, а конкретной датой: Mon Jan 2 15:04:05 MST 2006. Запомнить её легко: 01/02 03:04:05PM '06 -0700 (месяц, день, час, минута, секунда, год, часовой пояс).

Новички часто пытаются писать что-то вроде:

// ❌ Ошибка!
t, err := time.Parse("YYYY-MM-DDTHH:MM:SSZ", "2025-01-15T10:30:00Z")

Go просто не поймёт такой формат. Правильный layout для RFC 3339 — time.RFC3339:

// ✅ Правильно
t, err := time.Parse(time.RFC3339, "2025-01-15T10:30:00Z")
if err != nil {
    log.Fatal(err)
}
fmt.Println(t) // 2025-01-15 10:30:00 +0000 UTC

Константа time.RFC3339 равна строке "2006-01-02T15:04:05Z07:00". Обратите внимание на Z07:00 — это обозначение часового пояса.

Ошибка №2: Игнорирование наносекунд (time.RFC3339 vs time.RFC3339Nano)

Стандартный time.RFC3339 не поддерживает наносекунды. Если ваша строка содержит дробную часть секунды:

2025-01-15T10:30:00.123456789Z

…то парсинг с time.RFC3339 вернёт ошибку:

// ❌ Ошибка: parsing time "2025-01-15T10:30:00.123456789Z" as "2006-01-02T15:04:05Z07:00": cannot parse ".123456789Z" as "Z07:00"
t, err := time.Parse(time.RFC3339, "2025-01-15T10:30:00.123456789Z")

Решение — использовать time.RFC3339Nano:

// ✅ Правильно
t, err := time.Parse(time.RFC3339Nano, "2025-01-15T10:30:00.123456789Z")
if err != nil {
    log.Fatal(err)
}
fmt.Println(t) // 2025-01-15 10:30:00.123456789 +0000 UTC

Совет: Если вы не уверены, будут ли наносекунды, используйте time.RFC3339Nano — он обратно совместим и сработает для обоих случаев.

Ошибка №3: Часовой пояс — Z или +00:00 — это не одно и то же?

Строки 2025-01-15T10:30:00Z и 2025-01-15T10:30:00+00:00 означают одно и то же (UTC), но Go может их обработать по-разному, если layout указан неправильно.

// ✅ Оба варианта работают с time.RFC3339
t1, _ := time.Parse(time.RFC3339, "2025-01-15T10:30:00Z")
t2, _ := time.Parse(time.RFC3339, "2025-01-15T10:30:00+00:00")
fmt.Println(t1.Equal(t2)) // true

Но если вы пишете свой layout, будьте внимательны:

  • Z в layout означает, что строка должна содержать символ Z или смещение вида ±hh:mm.
  • -07:00 — только смещение.
  • Если в строке Z, а в layout указано -07:00, парсинг упадёт.

Лучший способ: не изобретайте велосипед — используйте готовые константы time.RFC3339 или time.RFC3339Nano.

Ошибка №4: Парсинг без учёта location (зоны)

Допустим, вы спарсили строку "2025-01-15T10:30:00" (без часового пояса). Go по умолчанию установит UTC. Это может привести к багам, если вы ожидаете местное время.

// Строка без зоны
t, _ := time.Parse("2006-01-02T15:04:05", "2025-01-15T10:30:00")
fmt.Println(t.Location()) // UTC — сюрприз!

Если вам нужно интерпретировать время в определённом часовом поясе, используйте time.ParseInLocation:

loc, _ := time.LoadLocation("Europe/Moscow")
t, _ := time.ParseInLocation("2006-01-02T15:04:05", "2025-01-15T10:30:00", loc)
fmt.Println(t) // 2025-01-15 10:30:00 +0300 MSK

Важно: ParseInLocation интерпретирует строку как время в указанном часовом поясе, не конвертируя его в UTC.

Ошибка

Блоги

Рекомендаций по книгам