26 июня 2024

🦫 Самоучитель по Go для начинающих. Часть 13. Работа с датой и временем. Пакет time

Энтузиаст-разработчик, автор статей по программированию.
В этой части самоучителя изучим способы работы с датами и временем в языке Go, разберем полезные функции пакета time и в заключение решим парочку интересных задач.
🦫 Самоучитель по Go для начинающих. Часть 13. Работа с датой и временем. Пакет 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
    

Давайте детально разберем каждую часть выведенного времени:

  1. Первая часть (2024-04-11 17:39:17.756388243) представляет собой дату и время в формате год-месяц-день час:минута:секунда.миллисекунда
  2. Вторая часть (+0300) указывает смещение временной зоны относительно UTC (Всемирного координированного времени) – это основной стандарт времени, используемый в авиации, картах, планах полетов, прогнозах погоды и других областях.
  3. Третья часть (MSK) указывает на часовой пояс. В примере это московское время.
  4. Четвертая часть (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" и так далее.

👨‍💻 Библиотека Go разработчика
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека Go разработчика»
🎓 Библиотека Go для собеса
Подтянуть свои знания по Go вы можете на нашем телеграм-канале «Библиотека Go для собеса»
🧩 Библиотека задач по Go
Интересные задачи по Go для практики можно найти на нашем телеграм-канале «Библиотека задач по Go»

Компоненты времени

Часто возникает необходимость взять только определенный отрезок времени, например, год, месяц или день. Этого можно добиться с помощью довольно очевидных функций:

        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 работают две вспомогательные функции:

  1. time.Since – вычисляет период между текущим и прошлым моментами времени, сокращение для time.Now().Sub(t)
  2. 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). Необходимо проверить соответствие даты следующим условиям:

  1. Дата приема не назначена на выходной день. Если это не так, выведите сообщение об ошибке с текстом: "Нельзя записаться на выходной день!".
  2. Дата приема не просрочена. Если это не так, выведите сообщение об ошибке с текстом: "Запись просрочена!".
  3. Время записи лежит в промежутке от 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).

⚠️ Примечание
Для корректного ввода даты понадобиться использовать вспомогательные функции, например, NewReader из пакета bufio.

Пример входных данных

        Дата рождения: 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 не ограничивается рассмотренными в этой статье функциями и предоставляет широкий набор полезных инструментов.

В следующей части самоучителя научимся с помощью встроенных пакетов обрабатывать текстовые данные, работать с файлами и взаимодействовать с операционной системой для выполнения часто встречающихся задач.

***

Содержание самоучителя

  1. Особенности и сфера применения Go, установка, настройка
  2. Ресурсы для изучения Go с нуля
  3. Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
  4. Переменные. Типы данных и их преобразования. Основные операторы
  5. Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
  6. Функции и аргументы. Области видимости. Рекурсия. Defer
  7. Массивы и слайсы. Append и сopy. Пакет slices
  8. Строки, руны, байты. Пакет strings. Хеш-таблица (map)
  9. Структуры и методы. Интерфейсы. Указатели. Основы ООП
  10. Наследование, абстракция, полиморфизм, инкапсуляция
  11. Обработка ошибок. Паника. Восстановление. Логирование
  12. Обобщенное программирование. Дженерики
  13. Работа с датой и временем. Пакет time
  14. Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
  15. Конкурентность. Горутины. Каналы
  16. Тестирование кода и его виды. Table-driven подход. Параллельные тесты
  17. Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net
  18. Протокол HTTP. Создание HTTP-сервера и клиента. Пакет net/http

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ