Как в Go хранится время
Язык программирования Go хранит время в соответствии с эпохой UNIX, которая берет свое начало 1 января 1970 года в 00:00:00 UTC. Системы на базе UNIX отслеживают время путем подсчета секунд, прошедших с этого особенного дня. Счетчик прошедших секунд хранится в виде 32-битного целого числа, которое изменяется в диапазоне от -2^32 до 2^31 – 1. Возникает логичный вопрос: зачем для подсчета времени использовать знаковый тип данных int32, если он содержит отрицательные значения? Дело в том, что отрицательными целыми числами представляется время до эпохи UNIX, а положительными – после неё. Таким образом, значение счетчика -100 означает момент времени за 100 секунд до 1 января 1970 года, а +100 секунд указывает на 100 секунд после этой даты.
Пакет time
Для работы с временем в Go используется пакет time стандартной библиотеки, содержащий обширный набор полезных функций и методов. Давайте на конкретных примерах рассмотрим основные из них.
Получить текущее локальное время можно с помощью функции time.Now
. Однако стоит учитывать, что для каждого запуска вывод времени будет отличаться. В статье приведены значения времени, актуальные на момент её написания:
fmt.Println(time.Now()) // 2024-04-11 17:39:17.756388243 +0300 MSK m=+0.000053694
Давайте детально разберем каждую часть выведенного времени:
- Первая часть (2024-04-11 17:39:17.756388243) представляет собой дату и время в формате
год-месяц-день час:минута:секунда.миллисекунда
- Вторая часть (+0300) указывает смещение временной зоны относительно UTC (Всемирного координированного времени) – это основной стандарт времени, используемый в авиации, картах, планах полетов, прогнозах погоды и других областях.
- Третья часть (MSK) указывает на часовой пояс. В примере это московское время.
- Четвертая часть (m=+0.000053694) содержит вспомогательную информацию, предоставляемую самим языком. В данном случае
m
обозначает момент времени, а+0.000053694
представляет его как количество секунд с начала выполнения программы или другого опорного момента.
Для вывода текущего времени в определенном формате следует использовать встроенные функции, такие как UTC(), Unix() и другие:
fmt.Println(time.Now().UTC()) // 2024-04-11 14:52:40.385906179 +0000 UTC
fmt.Println(time.Now().Unix()) // 1712847160
Каждый объект встроенной структуры time.Time
связан с конкретным местоположением, которое по своей сути является часовым поясом. Все локации определяются структурой time.Location
с неэкспортируемыми полями. Для получения информации о часовом поясе объекта времени используется метод Location
структуры Time, а задание определенной локации производится с помощью функции time.LoadLocation
и последующим вызовом метода In
:
location, err := time.LoadLocation("Europe/Samara")
if err != nil {
log.Fatal(err)
}
fmt.Println(time.Now())
fmt.Println(time.Now().In(location))
fmt.Println(time.Now().In(location).Location())
Вывод выглядит следующим образом:
2024-04-12 09:26:00.032310773 +0300 MSK m=+0.000709391
2024-04-12 10:26:00.032383371 +0400 +04
Europe/Samara
Если название локации содержит пустую строку или "UTC", то функция LoadLocation возвращает время в соответствии с UTC. При указании "Local" вернется локальное время. Во всех иных случаях наименование локации должно соответствовать названиям из базы данных часовых поясов IANA. Например, "Europe/Moscow", "Asia/Tomsk" и так далее.
Компоненты времени
Часто возникает необходимость взять только определенный отрезок времени, например, год, месяц или день. Этого можно добиться с помощью довольно очевидных функций:
now := time.Now()
fmt.Println("Год:", now.Year())
fmt.Println("Месяц:", now.Month())
fmt.Println("День:", now.Day())
fmt.Println("Час:", now.Hour())
fmt.Println("Минута:", now.Minute())
fmt.Println("Секунда:", now.Second())
fmt.Println("Наносекунда:", now.Nanosecond())
Задание момента времени
Если необходимо создать временной объект для определенной даты, то следует воспользоваться функцией time.Date
со следующей сигнатурой:
func Date(year int, month Month, day, hour, min, sec, nsec int, loc *Location) Time
Функция time.Date
принимает в качестве параметров компоненты времени в формате год, месяц, день, час, минута, секунда, наносекунда, местоположение
. Стоит отметить, что эти параметры могут находиться за пределами допустимых значений, но в результате все равно будут сконвертированы. Например, 34 апреля будет воспринято как 4 мая:
t := time.Date(2024, time.April, 34, 10, 9, 8, 7, time.UTC)
fmt.Println(t) // 2024-05-04 10:09:08.000000007 +0000 UTC
Форматирование времени
Форматирование времени в Go производится с помощью метода time.Format()
, который конвертирует объект структуры Time в строку, соответствующую по параметру layout:
func (t Time) Format(layout string) string
Стоит отметить, что шаблоны для time.Parse
и time.Format
предопределены в пакете time. Базовое время, используемое в них, представляет собой конкретную отметку: 01/02 03:04:05PM '06 -0700
, что в формате Unix выглядит как Mon Jan 2 15:04:05 MST 2006
.
Документация пакета time допускает использование следующих форм времени:
Year: "2006" "06"
Month: "Jan" "January" "01" "1"
Day of the week: "Mon" "Monday"
Day of the month: "2" "_2" "02"
Day of the year: "__2" "002"
Hour: "15" "3" "03" (PM or AM)
Minute: "4" "04"
Second: "5" "05"
AM/PM mark: "PM"
Для форматирования произвольного объекта времени следует использовать константы из эталонного времени, переупорядочив их в необходимом порядке. К примеру, написанный ниже код выведет на экран время у заданной даты в формате "15:04:05":
t := time.Date(2024, time.April, 11, 10, 9, 8, 7, time.UTC)
fmt.Println("Время:", t.Format("15:04:05")) // Время: 10:09:08
Форматирование может производиться по предопределенным шаблонам, таким как DateOnly ("2006-01-02"), DateTime ("2006-01-02 15:04:05"), TimeOnly ("15:04:05") и другим:
// полный список шаблонов можно найти в документации пакета time
t := time.Date(2024, time.April, 11, 10, 9, 8, 7, time.UTC)
fmt.Println("Время: ", t.Format(time.TimeOnly))
fmt.Println("Дата:", t.Format(time.DateOnly))
fmt.Println("Временная метка (timestamp):", t.Format(time.Stamp))
fmt.Println("UnixDate:", t.Format(time.UnixDate))
На экран будет выведено следующее:
Время: 10:09:08
Дата: 2024-04-11
Временная метка (timestamp): Apr 11 10:09:08
UnixDate: Thu Apr 11 10:09:08 UTC 2024
Парсинг времени
Функция time.Parse
парсит форматированную строку и возвращает значение времени, которое она представляет. Иначе говоря, конвертирует строку в структуру Time:
func Parse(layout, value string) (Time, error)
Для корректного использования функции time.Parse
стоит учитывать несколько нюансов. Во-первых, второй параметр функции должен быть доступен для парсинга с использованием шаблона, указанного в качестве первого аргумента. Во-вторых, при работе с time.Parse
допускается использовать только предопределенную в языке модель для описания ввода и вывода, рассмотренную ранее. В-третьих, time.Parse
при отсутствии часового пояса в переданной строке возвращает время в формате UTC:
parsedTime, err := time.Parse("2 Jan 2006 03:04AM", "11 Apr 2024 10:25AM")
if err != nil {
fmt.Println(err)
}
fmt.Println(parsedTime) // 2024-04-11 10:25:00 +0000 UTC
Длительность
Продолжительность между двумя промежутками времени в наносекундах представляет тип Duration: type Duration int64
, ограниченный примерно 290 годами.
Вычислить продолжительность промежутка времени можно с помощью функции time.Sub
:
func (t Time) Sub(u Time) Duration
Рассмотрим применение функции time.Sub
на примере, где зададим три отметки времени с разницей в 2 часа 4 минуты и вычислим разницу между ними:
date := time.Date(2024, time.April, 11, 10, 9, 8, 7, time.UTC)
future := time.Date(2024, time.April, 11, 12, 13, 8, 7, time.UTC)
past := time.Date(2024, time.April, 11, 8, 5, 8, 7, time.UTC)
fmt.Println(date.Sub(future)) // -2h4m0s
fmt.Println(date.Sub(past)) // 2h4m0s
На основе time.Sub
работают две вспомогательные функции:
time.Since
– вычисляет период между текущим и прошлым моментами времени, сокращение дляtime.Now().Sub(t)
time.Until
– вычисляет период между текущим и будущим моментами времени, сокращение дляt.Sub(time.Now())
Рассмотрим эти функции на примере программы, в которой создадим два момента времени (будущее и прошлое) с разницей в три часа по сравнению с текущим и вычислим временные промежутки, округлив окончательный результат до секунд с помощью метода Round:
future := time.Date(2024, time.April, 12, 13, 21, 9, 8, time.Local)
past := time.Date(2024, time.April, 12, 7, 21, 9, 8, time.Local)
fmt.Println(time.Now().Local()) // 2024-04-12 10:22:58.804074995 +0300 MSK
fmt.Println(time.Until(future).Round(time.Second)) // 2h58m10s
fmt.Println(time.Since(past).Round(time.Second)) // 3h1m50s
Арифметика времени
Ранее для добавления или вычитания времени мы использовали явное указание его компонентов в функции time.Date. Такой подход довольно неудобный и подвержен ошибкам.
Для более точной и корректной временной арифметики следует использовать функции time.Add
и time.AddDate
:
func (t Time) Add(d Duration) Timefunc (t Time) AddDate(years int, months int, days int) Time
При указании положительных параметров будет производиться добавление времени, при отрицательных – вычитание:
now := time.Date(2024, time.April, 11, 10, 9, 8, 7, time.UTC)
fmt.Println(now.Add(time.Hour * 2)) // + 2 часа
fmt.Println(now.AddDate(1, 3, 10)) // + 1 год 3 месяца 10 дней
fmt.Println(now.Add(time.Minute * (-6))) // - 6 минут
В результате выполнения кода увидим следующее:
2024-04-11 12:09:08.000000007 +0000 UTC
2025-07-21 10:09:08.000000007 +0000 UTC
2024-04-11 10:03:08.000000007 +0000 UTC
Сравнить два временных объекта t1 и t2 можно с помощью следующих функций: time.Equal
(t1 равен t2), time.Before
(t1 произошел до t2) и time.After
(t1 произошел после t2):
now := time.Now()
future := now.Add(time.Hour*3 + time.Minute*3)
past := now.Add(-time.Hour*3 - time.Minute*3)
fmt.Println(now.Equal(past)) // false
fmt.Println(now.Before(future)) // true
fmt.Println(now.After(past)) // true
Приостановка программы
В некоторых ситуациях требуется приостановить выполнение программы на определенный промежуток времени. В этом помогает функция time.Sleep
, которая останавливает текущую горутину на указанное в параметрах время. При этом отрицательное или нулевое значение параметра приводит к немедленному завершению time.Sleep и, соответственно, игнорированию задержки.
Следующий код выведет строку "start...", остановит выполнение главной горутины main на 3 секунды и по прошествии этого времени выведет строку "end after 3 seconds":
func main() {
fmt.Println("start...")
time.Sleep(3 * time.Second)
fmt.Println("end after 3 seconds")
}
Задачи
Самое время закрепить изученную теорию на практике, решив несколько несложных задач. Настоятельно рекомендуется не игнорировать возможные ошибки, а обрабатывать их с помощью изученных ранее конструкций, например, log.Fatal(err)
.
Преобразование времени
Напишите программу для преобразования строкового представления времени в формате 2000-05-14T07:30:00+07:00
в структуру Time формата Unix Date: Sun May 14 07:30:00 +0700 2000
. Для нахождения правильного шаблона обратитесь к списку констант пакета time.
Пример входных данных
2000-05-14T07:30:00+07:00
Выходные данные для примера
Sun May 14 07:30:00 +0700 2000
Решение
func main() {
var s string
fmt.Scan(&s)
t, err := time.Parse(time.RFC3339, s) // парсинг по шаблону time.RFC3339
if err != nil {
panic(err)
}
fmt.Println(t.Format(time.UnixDate)) // вывод по шаблону time.UnixDate
}
Запись к врачу
Напишите программу для имитации системы записи на прием к врачу. На вход подается строка с датой в формате “Mon Jan _2 15:04:05 2006” (time.ANSIC). Необходимо проверить соответствие даты следующим условиям:
- Дата приема не назначена на выходной день. Если это не так, выведите сообщение об ошибке с текстом: "Нельзя записаться на выходной день!".
- Дата приема не просрочена. Если это не так, выведите сообщение об ошибке с текстом: "Запись просрочена!".
- Время записи лежит в промежутке от 8 до 20 включительно. Если это не так, выведите сообщение об ошибке с текстом: "Врач работает с 8 до 20 часов!".
В случае соответствия даты всем условиям выведите сообщение с текстом: "Вы успешно записались на %s\n", где вместо спецификатора %s находится дата записи, отформатированная в соответствии с шаблоном "Monday, January 2, 2006, at 15:04".
Пример входных данных
Mon Apr 22 15:00:00 2024
Выходные данные для примера
Вы успешно записались на Monday, April 22, 2024, at 15:00
Решение
func main() {
date := "Sun Apr 21 15:00:00 2024"
t, err := time.Parse(time.ANSIC, date)
if err != nil {
log.Fatal(err)
}
if t.Weekday() == time.Saturday || t.Weekday() == time.Sunday {
log.Fatal("Нельзя записаться на выходной день!")
}
if t.Before(time.Now()) {
log.Fatal("Запись просрочена!")
}
if t.Hour() < 8 || t.Hour() > 20 {
log.Fatal("Врач работает с 8 до 20 часов!")
}
fmt.Printf("Вы успешно записались на %s\\n", t.Format("Monday, January 2, 2006, at 15:04"))
}
Сколько лет Гоше?
Первоклассник Гоша очень хочет узнать, сколько ему полных лет в данный момент времени, но не может сделать это с большой точностью. Помогите мальчику вычислить свой возраст, а именно: количество лет, месяцев, недель, дней, часов и минут, прошедших с заданной во входных параметрах даты в формате "2006-01-02 15:04:05" (time.DateTime).
Пример входных данных
Дата рождения: 2005-04-14 10:00:00
Выходные данные для примера
Лет: 19.013818
Месяцев: 228.165821
Недель: 991.434818
Дней: 6940.043727
Часов: 166561.049451
Минут: 9993662.967034
Решение
func main() {
fmt.Print("Дата рождения: ")
// чтение входных данных до символа переноса строки
input, err := bufio.NewReader(os.Stdin).ReadString('\\\\n')
if err != nil {
log.Fatal(err)
}
input = input[:len(input)-1] // удаление переноса строки
// парсинг времени по шаблону time.DateTime
t, err := time.Parse(time.DateTime, input)
if err != nil {
log.Fatal(err)
}
// вычисление количества дней, прошедших с времени t
days := time.Since(t).Hours() / 24
fmt.Printf("Лет: %f\\\\nМесяцев: %f\\\\nНедель: %f\\\\nДней: %f\\\\nЧасов: %f\\\\nМинут: %f\\\\n",
days/365, (days/365)*12, days/7, days, days*24, days*1440)
}
Сколько еще работать?
Напишите программу для подсчета количества рабочих дней между начальной и конечной датами (включительно). Данные вводятся в формате "2006-01-02" (time.DateOnly
). Также требуется добавить проверку того, что начальная дата была раньше конечной. Если это условие не выполняется, завершите программу с текстом: "Начальная дата должна быть раньше конечной".
Пример входных данных
Начальная дата в формате ГГГГ-ММ-ДД: 2024-04-08
Конечная дата в формате ГГГГ-ММ-ДД: 2024-04-14
Выходные данные для примера
Количество рабочих дней между 2024-04-08 и 2024-04-14 (включительно): 5
Решение
func main() {
var start, end string
fmt.Print("Начальная дата в формате ГГГГ-ММ-ДД: ")
fmt.Scan(&start)
fmt.Print("Конечная дата в формате ГГГГ-ММ-ДД: ")
fmt.Scan(&end)
// парсинг начальной даты
startTime, err := time.Parse(time.DateOnly, start)
if err != nil {
log.Fatal(err)
}
// парсинг конечной даты
endTime, err := time.Parse(time.DateOnly, end)
if err != nil {
log.Fatal(err)
}
// проверка того, что начальная дата была раньше конечной
if endTime.Sub(startTime).Hours() < 0 {
log.Fatal("Начальная дата должна быть раньше конечной")
}
var workdays int // счетчик рабочих дней
currentTime := startTime
// цикл до тех пор, пока начальное время меньше конечного
for !currentTime.After(endTime) {
// проверка текущей даты на выходной день
if currentTime.Weekday() != time.Saturday &&
currentTime.Weekday() != time.Sunday {
workdays++ // увеличение счетчика рабочих дней
}
currentTime = currentTime.AddDate(0, 0, 1) // добавление одного дня
}
// форматированный вывод
fmt.Printf("Количество рабочих дней между %s и %s (включительно): %d\\\\n",
startTime.Format("2006-01-02"),
endTime.Format("2006-01-02"),
workdays)
}
Заключение
В этом уроке мы подробно изучили способ хранения времени в Go, а также основные функции и методы пакета time: Now, Date, Format, Parse, Sub, Since, Until и некоторые другие. Их использование откроет перед вами новые возможности для обработки временных объектов в программах. Стоит отметить, что пакет time не ограничивается рассмотренными в этой статье функциями и предоставляет широкий набор полезных инструментов.
В следующей части самоучителя научимся с помощью встроенных пакетов обрабатывать текстовые данные, работать с файлами и взаимодействовать с операционной системой для выполнения часто встречающихся задач.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
Комментарии