Golang → Дата, время и часовые пояса в Golang
Можно сказать, что сейчас будет краткий пересказ документации и всё элементарно, но когда столкнулся с необходимостью работать с таймзонами, то пришлось поискать информацию и примеры. Так что пусть будет 🙂
Создадим объект даты и времени, соответствующий данному моменту времени. Этим в Go занимается пакет time
1 2 3 4 5 6 7 8 9 10 11 12 | package main
import (
"fmt"
"time"
)
func main() {
dt := time.Now()
fmt.Println("Current time:", dt)
}
|
В результате увидим:
1 | Current time: 2024-04-29 12:52:04.130203218 +0300 MSK
|
Создадим объекты даты и времени, соответствующие конкретному моменту времени. В явном виде и через его текстовое представление. Последний вариант на практике приходится использовать куда чаще
1 2 3 4 5 6 7 8 | dt1 := time.Date(2024, time.April, 29, 12, 52, 4, 0, time.Local)
dt2, err := time.Parse("2006-01-02 15:04:05", "2024-04-29 12:52:04")
if err != nil {
panic(err)
}
fmt.Println("Date and time:", dt1)
fmt.Println("Date and time:", dt2)
|
Результат:
1 2 | Date and time: 2024-04-29 12:52:04 +0300 MSK
Date and time: 2024-04-29 12:52:04 +0000 UTC
|
Видно, что когда в строке со временем не указан часовой пояс, то функция time.Parse интепретирует результат во всемирном координированном времени UTC. Так же промелькнула переменная time.Local, которая представляет местный часовой пояс системы.
Вывод времени в заданном формате:
1 2 3 4 5 | dt := time.Now()
fmt.Println("RFC822:", dt.Format(time.RFC822))
fmt.Println("Unix: ", dt.Format(time.UnixDate))
fmt.Println("Custom:", dt.Format("2006/01/02 3:04 PM"))
|
И результат:
1 2 3 | RFC822: 29 Apr 24 20:05 MSK
Unix: Mon Apr 29 20:05:39 MSK 2024
Custom: 2024/04/29 8:05 PM
|
Для произвольного форматирования времени приходится заглядывать в файл $GOROOT/src/time/format.go, поэтому тоже сюда выпишу :)
Год: | "2006" "06" |
Месяц: | "Jan" "January" "01" "1" |
День: | "2" "_2" "02" |
День недели: | "Mon" "Monday" |
День в году: | "__2" "002" |
Часы: | "15" "3" "03" (для PM или AM) |
Минуты: | "4" "04" |
Секунды: | "5" "05" |
смещение для часовых поясов | |
-0700 | ±hhmm |
-07:00 | "±hh:mm |
Z0700 | Z или ±hhmm |
Z07:00 | Z или ±hh:mm |
Отдельные компоненты даты и времени
Тут всё просто, составляющие можно получить сразу для даты, или сразу для времени, а можно и поштучно извлечь каждый
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | dt := time.Now()
year, month, day := dt.Date()
fmt.Println("Year:\t", year)
fmt.Println("Month:\t", month)
fmt.Println("Day:\t", day)
hour, min, sec := dt.Clock()
fmt.Println("Hour:\t", hour)
fmt.Println("Minute:\t", min)
fmt.Println("Sec:\t", sec)
fmt.Println("----")
fmt.Println("Day:\t", dt.Day())
fmt.Println("Hour:\t", dt.Hour())
fmt.Println("Minute:\t", dt.Minute())
|
Результат:
1 2 3 4 5 6 7 8 9 10 | Year: 2024
Month: May
Day: 3
Hour: 18
Minute: 12
Sec: 40
----
Day: 3
Hour: 18
Minute: 12
|
Арифметические операции со временем
К арифметическим операциям относятся функции, производимые со временем, такие как сложение, вычитание и разница во времени. Сложение и вычитание производится функцией (time.Time).Add(time.Duration) (для вычитания нужны отрицательные значения параметров). Существует также функция (time.Time).AddDate(y,m,d int), которая принимает 3 параметра: годы, месяцы и дни для выполнения сложения или вычитания.
Следующий код демонстрирует использование этих функций:
1 2 3 4 5 6 7 8 9 10 | dt := time.Now()
tomorrow := dt.Add(24 * time.Hour)
fmt.Println("Current time:\t", dt.Format(time.DateTime))
fmt.Println("Tomorrow:\t", tomorrow.Format(time.DateTime))
next := dt.AddDate(0, 0, 7)
fmt.Println("Next Week:\t", next.Format(time.DateTime))
|
Результат:
1 2 3 | Current time: 2024-05-04 00:11:33
Tomorrow: 2024-05-05 00:11:33
Next Week: 2024-05-11 00:11:33
|
Для нахождения разницы во времени используется функция (time.Time).Sub(time.Time)
1 2 3 4 5 6 7 | dt := time.Now()
past := time.Date(2022, time.December, 31, 14, 20, 0, 0, time.Local)
diff := dt.Sub(past)
fmt.Println("Diff:\t", diff)
fmt.Println("Days:\t", int(diff.Hours()/24))
|
Результат:
1 2 | Diff: 11746h1m8.733176611s
Days: 489
|
Естественно, это не все имеющиеся манипуляции со временем, но у меня нет цели перепечатывать документацию стандартной библиотеки, тут только самое основное 😎
Логические операции
Т.е. сравнения. Тут уже код скажет сам за себя, без комментариев. Хотя нет, стоит упомянуть, что при сравнениях учитывается и часовой пояс, так что при сравнении двух объектов времени на равенство нужно использовать метод (time.Time).Equal(time.Time), а не ==
1 2 3 4 5 6 7 8 9 10 11 12 13 | dt1 := time.Date(2024, time.July, 11, 10, 0, 0, 0, time.Local)
dt2 := time.Date(2024, time.August, 1, 10, 0, 0, 0, time.Local)
before := dt1.Before(dt2)
after := dt1.After(dt2)
fmt.Printf("dt1.Before(dt2) = %v\n", before)
fmt.Printf("dt1.After(dt2) = %v\n", after)
dt3, _ := time.Parse(time.RFC822Z, "02 Jan 25 16:04 +0200")
dt4, _ := time.Parse(time.RFC822Z, "02 Jan 25 18:04 +0400")
fmt.Printf("dt3.Equal(dt4) = %v\n", dt3.Equal(dt4))
|
Результат:
1 2 3 | dt1.Before(dt2) = true
dt1.After(dt2) = false
dt3.Equal(dt4) = true
|
Часовые пояса
При работе со временем и часовыми поясами возможно несколько ситуаций, когда временная зона имеется в строковом представлении времени, когда она известна заранее, но в строке со временем её нет, и когда известно смещение относительно UTC (например, OffsetTime в Exif-информации фотографий). Пример кода для первых двух случаев, допустим, имеем время созвона команды из Новосибирска (UTC+7:00) и нужно посмотреть, сколько это будет по Москве, чтобы тоже поучаствовать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | dateTimeStr1 := "2024-05-02T15:28:00+07:00"
dateTimeStr2 := "2024-05-02 15:28:00"
dt1, err := time.Parse(time.RFC3339, dateTimeStr1)
if err != nil {
panic(err)
}
locationNsk, err := time.LoadLocation("Asia/Novosibirsk")
if err != nil {
panic(err)
}
dt2, err := time.ParseInLocation(time.DateTime, dateTimeStr2, locationNsk)
if err != nil {
panic(err)
}
fmt.Println("date-time var.1: ", dt1)
fmt.Println("date-time var.2: ", dt2)
locationMsk, err := time.LoadLocation("Europe/Moscow")
if err != nil {
panic(err)
}
fmt.Println("date-time in MSK:", dt1.In(locationMsk))
|
Результат:
1 2 3 | date-time var.1: 2024-05-02 15:28:00 +0700 +0700
date-time var.2: 2024-05-02 15:28:00 +0700 +07
date-time in MSK: 2024-05-02 11:28:00 +0300 MSK
|
Теперь случай, когда известно смещение времени относительно UTC, пусть это будет UTC-2, используем time.FixedZone
1 2 3 4 5 6 7 8 9 10 | offset := -2
tz := time.FixedZone("", offset*3600)
dt, err := time.ParseInLocation(time.DateTime, "2024-08-11 11:30:00", tz)
if err != nil {
panic(err)
}
fmt.Println("Datetime object: ", dt)
fmt.Println("Datetime object: ", dt.In(time.Local))
|
Результат:
1 2 | Datetime object: 2024-08-11 11:30:00 -0200 -0200
Datetime object: 2024-08-11 16:30:00 +0300 MSK
|
Если же для обозначения часового пояса используется аббревиатура, то тогда могут быть неожиданности с тем, что программа будет считать часовой пояс неизвестным, создаст виртуальную локацию с тем же названием и определит ей нулевое смещение относительно UTC, а вот распознается или нет эта самая аббревиатура часового пояса, будет зависеть от локации, в которой разбирается строка в временем. Ниже пример с часовым поясом CET (Central Europe Time, т.е. центрально-европейское время, UTC+1)
1 2 3 4 5 6 7 8 9 10 11 12 | dt, _ := time.Parse(time.RFC822, "24 Feb 22 04:00 CET")
fmt.Println("Datetime object:", dt)
fmt.Println("Datetime object:", dt.In(time.Local))
fmt.Println("----------------")
loc, _ := time.LoadLocation("Europe/Amsterdam")
dt2, _ := time.ParseInLocation(time.RFC822, "24 Feb 22 04:00 CET", loc)
fmt.Println("Datetime object:", dt2)
fmt.Println("Datetime object:", dt2.In(time.Local))
|
Результат:
1 2 3 4 5 | Datetime object: 2022-02-24 04:00:00 +0000 CET
Datetime object: 2022-02-24 07:00:00 +0300 MSK
----------------
Datetime object: 2022-02-24 04:00:00 +0100 CET
Datetime object: 2022-02-24 06:00:00 +0300 MSK
|
В общем, видно, что в первом случае смещение времени определено неправильно и дальнейшая работа с таким объектом даты-времени может дать некорректные результаты, если смещение времени между поясами имеет значение. Не знаю, почему так реализовали работу с часовыми поясами в пакете time, но имеем то, что имеем. Возможно, это связано с тем, что пакет time работает не со всеми известными и возможными аббериатурами часовых поясов, а с теми, которые определены в базе данных IANA. Также имеется у аббревиатур часовых поясов и проблема с уникальностью, когда нельзя однозначно определить к какому часовому поясу относится запись времени, например для IST есть три различных варианта: India Standard Time (UTC+5:30), Irish Standard Time (UTC+1) и Israel Standard Time (UTC+2). И этот пример далеко не единственный. Тогда действительно для корректного определения времени нужна дополнительная информация о локации. Если же работать с аббервиатурами часовых поясов всё же нужно, то может пригодиться сторонняя библиотека вроде go-timezone
Комментарии