Парсинг даты и времени в формате RFC-3339 / ISO-8601 в Go
Основная проблема: неверный форматный шаблон
Вы почти правильно поняли идею, но в Go есть своя магия с эталонной датой. Вместо произвольных символов вроде YYYY-MM-DD используется фиксированная дата: Mon Jan 2 15:04:05 MST 2006 (или сокращённо: 01/02 03:04:05PM '06 -0700). Это нужно запомнить: год — 2006, месяц — 01, день — 02, час — 15 (или 03 с AM/PM), минута — 04, секунда — 05, часовой пояс — -07:00 или Z07:00.
Ваша ошибка возникла из-за того, что символ Z в строке "2023-12-25T15:30:00Z" обозначает UTC (часовой пояс +00:00), а в шаблоне вы указали Z07:00. Go пытается найти букву Z, а затем ожидает смещение, но в исходной строке после Z ничего нет — отсюда и ошибка.
Правильное решение
Для парсинга строки "2023-12-25T15:30:00Z" используйте один из двух шаблонов:
time.RFC3339— встроенная константа ="2006-01-02T15:04:05Z07:00"time.RFC3339Nano— с наносекундами
Но есть нюанс: time.RFC3339 ожидает, что если часовой пояс указан как Z, то это именно UTC. А если указан +03:00 — то смещение. Ваш шаблон "2006-01-02T15:04:05Z07:00" требует, чтобы обязательно было и Z, и смещение, что неверно.
Используйте time.RFC3339 напрямую:
package main
import (
"fmt"
"time"
)
func main() {
// Пример 1: с Z (UTC)
dateStr1 := "2023-12-25T15:30:00Z"
parsedTime1, err := time.Parse(time.RFC3339, dateStr1)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("UTC:", parsedTime1)
// Пример 2: со смещением +03:00
dateStr2 := "2023-12-25T18:30:00+03:00"
parsedTime2, err := time.Parse(time.RFC3339, dateStr2)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("С часовым поясом:", parsedTime2)
// Пример 3: с наносекундами
dateStr3 := "2023-12-25T15:30:00.123456789Z"
parsedTime3, err := time.Parse(time.RFC3339Nano, dateStr3)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("С наносекундами:", parsedTime3)
}
Что делать, если формат отличается?
Если вы получаете даты в нестандартном формате (например, без секунд, с пробелом вместо T, или с дробной частью для секунд), можно создать свой шаблон. Вот несколько примеров:
// Без секунд: "2023-12-25T15:30Z"
layout1 := "2006-01-02T15:04Z07:00"
// С пробелом вместо T: "2023-12-25 15:30:00+03:00"
layout2 := "2006-01-02 15:04:05Z07:00"
// Дробные секунды (миллисекунды): "2023-12-25T15:30:00.000Z"
layout3 := "2006-01-02T15:04:05.000Z07:00"
// .000 — миллисекунды, .000000 — микросекунды, .000000000 — наносекунды
Совет профессионала
Если вы работаете с JSON, рекомендую использовать time.Time как тип поля в структуре. Пакет encoding/json умеет парсить RFC-3339 автоматически:
type Event struct {
Name string `json:"name"`
Date time.Time `json:"date"` // парсится автоматически!
}
func main() {
jsonStr := `{"name": "Test", "date": "2023-12-25T15:30:00Z"}`
var event Event
if err := json.Unmarshal([]byte(jsonStr), &event); err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println(event.Date)
}
Это самый надёжный способ для работы с API.
<<Альтернативный подход: использование time.ParseInLocation
Если вы знаете, что все даты приходят в определённом часовом поясе (например, всегда UTC или всегда Europe/Moscow), можно сначала распарсить строку без учёта временной зоны, а затем применить нужный часовой пояс с помощью time.ParseInLocation.
package main
import (
"fmt"
"time"
)
func main() {
dateStr := "2023-12-25T15:30:00" // обратите внимание: без Z и смещения
layout := "2006-01-02T15:04:05"
// Предположим, что все даты в часовом поясе Москвы (UTC+3)
loc, _ := time.LoadLocation("Europe/Moscow")
parsedTime, err := time.ParseInLocation(layout, dateStr, loc)
if err != nil {
fmt.Println("Ошибка:", err)
return
}
fmt.Println("Время в Москве:", parsedTime)
fmt.Println("Время в UTC:", parsedTime.UTC())
}
Когда это полезно?
- Когда источник данных не указывает часовой пояс, но вы точно знаете, в каком он поясе.
- Когда нужно унифицировать все даты к одному часовому поясу (например, всегда сохранять в UTC).
- Для обратной совместимости со старыми системами, которые отдают даты без временной зоны.
Также можете использовать time.FixedZone для создания кастомного часового пояса, если стандартное имя не подходит:
loc := time.FixedZone("MSK", 3*60*60) // +3 часа
Этот подход даёт больше контроля, но требует аккуратности: если в строке всё же будет указан часовой пояс, ParseInLocation его проигнорирует и применит заданную вами локацию. Поэтому используйте его только когда уверены в формате данных.