Avatar ·

Парсинг даты и времени в формате RFC-3339 / ISO-8601 в Go

Всем привет! Я только начал изучать Go и столкнулся с проблемой парсинга строки даты и времени. Мне нужно обработать данные из JSON API, где даты приходят в формате "2023-12-25T15:30:00Z" или "2023-12-25T18:30:00+03:00" (это вроде бы RFC-3339 или ISO-8601). Я знаю, что в Go есть пакет `time` с функцией `time.Parse`, но не могу понять, какой форматный шаблон указать. Я пробовал что-то вроде `"2006-01-02T15:04:05Z07:00"`, но получаю ошибки. Вот мой код: ```go package main import ( "fmt" "time" ) func main() { dateStr := "2023-12-25T15:30:00Z" layout := "2006-01-02T15:04:05Z07:00" parsedTime, err := time.Parse(layout, dateStr) if err != nil { fmt.Println("Ошибка парсинга:", err) return } fmt.Println(parsedTime) } ``` Выдаёт ошибку: `parsing time "2023-12-25T15:30:00Z" as "2006-01-02T15:04:05Z07:00": cannot parse "Z" as "Z07:00"`. Я перепробовал много вариантов шаблонов, но ничего не работает. Подскажите, как правильно парсить такие строки? И как быть, если часовой пояс указан как `+03:00`? Заранее спасибо! <<>>

Основная проблема: неверный форматный шаблон

Вы почти правильно поняли идею, но в 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 его проигнорирует и применит заданную вами локацию. Поэтому используйте его только когда уверены в формате данных.