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

Полезные ссылки

Аббервиатуры часовых поясов: Wiki, ещё

Комментарии

0 комментариев Написать что-нибудь
Или войдите, чтобы не заполнять форму:
Адрес электронной почты нигде не отображается, необходим только для обратной связи.
Напрограммировано на Go 1.23.2, версия движка 589441d