🦫 Самоучитель по Go для начинающих. Часть 13. Работа с датой и временем. Пакет time
В этой части самоучителя изучим способы работы с датами и временем в языке Go, разберем полезные функции пакета time и в заключение решим парочку интересных задач.
Как в 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
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
- Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http