26 июня 2024

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

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

Работа мечты в один клик 💼

Работа в Сбере: пройди собеседование и получи оффер за 15 минут

💭Мечтаешь работать в Сбере, но не хочешь проходить десять кругов HR-собеседований? Теперь это проще, чем когда-либо!

💡AI-интервью за 15 минут – и ты уже на шаг ближе к своей новой работе.

Как получить оффер?
📌 Зарегистрируйся
📌 Пройди AI-интервью
📌 Получи обратную связь сразу же!

HR больше не тянут время – рекрутеры свяжутся с тобой в течение двух дней! 🚀

Реклама. ПАО СБЕРБАНК, ИНН 7707083893. Erid 2VtzquscAwp


Как в 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

Комментарии

 
 
05 июля 2024

Нравится стиль повествования в статьях всего этого курса. Сейчас как раз увлекся освоением языка, из документации пакетов почти не вылажу...английский текст утомляет. Такие вот обзорные статьи на русском языке очень хорошо заходят.

Ожидаю, когда дойдем до...

  • работы с флагами и аргументами командной строки;
  • работы с файлами;
  • запуска простого сервера и обработки запросов;
  • обмена данными с какой-нибудь БД.

Автору благодарность! Надеюсь, новая статья уже скоро.

Спасибо за ваш отзыв! Очень рады, что вам нравится стиль повествования и что статьи помогают в освоении языка.

На следующей неделе выйдет «Самоучитель по Go для начинающих. Часть 14. Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os»

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

LIVE >

Подпишись

на push-уведомления