Найти тему
Я, Golang-инженер

#32. Дата-время в Golang и длительность операций с файлами

Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой! Джедаи и Амазонки!

Разберём основы форматирования даты и времени. Будет много примеров. Также поделюсь лайфхаками, как сделать код лаконичнее. Go!

Время и дата

Время и дата - важный элемент backend'a, например для ведения log-журнала событий.

Задача: вывести в терминал время выполнения конкретной операции. Пример простейшего кода для вывода даты и времени в терминал:

Код время-дата
Код время-дата

Программа выдаст примерно следующий результат в IDE:

Результат выполнения кода
Результат выполнения кода

Либо та же программа, выполненная в песочнице Replit:

Вывод кода в песочнице Replit
Вывод кода в песочнице Replit

Что мы видим интересного на этих двух записях в терминале?

  • 2023-02-20 - понятно что это: год - месяц - день.
  • Дальше тоже всё ясно - часы - минуты - секунды.

Что после точки?

>>> 2023-02-20 22:23:28.3768864 +1000 +10 m=+0.002000101

.3768864 - доли секунды:

  • первые три цифры после точки (376) - это миллисекунды (ms);
  • следующие три цифры (886) - микросекунды (µs).
  • И так далее - разные IDE выдаёт разное количество знаков после точки.

>>> 2023-02-20 22:23:28.3768864 +1000 +10 m=+0.002000101

+1000 +10 - это отображение часового пояса.

>>> 2023-02-20 22:23:28.3768864 +1000 +10 m=+0.002000101

m=+0.002000101 - монотонные часы. Подробнее о них можно почитать в документации Go.

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

1. Методы year, month, hour и т.д.

Форматирование времени с помощью методов
Форматирование времени с помощью методов

Также есть методы Nanosecond(), YearDay(), Weekday() и другие.

А теперь вызовем функцию Printf и упростим синтаксис:

Форматирование времени Printf
Форматирование времени Printf

Итак, мы получили приемлемый вывод дата-время. Сравните:

Было: 2023-02-20 23:10:12.6852837 +1000 +10 m=+0.001000001;

Стало: 2023-2-20 23:23:12.

Это громоздкий метод, есть попроще.

2. Метод Format

Рассмотрим код:

Код с методом Format
Код с методом Format

Метод Format использует строку "2006-01-02 15:04:05" для отображения текущего времени - 2023-02-20 23:28:21. Что это за стркоа?

Это шаблон времени в Go, где комбинация 2006 заменяет метод Year из предыдущего примера, комбинация 01 - заменяет метод Month и т.д.

Вопрос: почему именно такая странная дата - второе января 2006-го, используется в качестве шаблона?

Ответ: дату 2006-01-02 15:04:05, которую Go использует как шаблон, в общем виде можем представить так:

01/02 03:04:05PM '06 -0700

Если вы посмотрите на каждый элемент даты и времени, вы увидите, что они увеличиваются на единицу в каждом последующем элементе:

  1. Указан месяц 01;
  2. Указан день месяца 02;
  3. Указан час 03;
  4. Минута 04;
  5. Секунда 05;
  6. Год - 06 (точнее, 2006);
  7. Часовой пояс - 07.

Вот такое объяснение появления шаблона.

Больше вариантов, как задавать время с помощью метода Format, рекомендую смотреть в официальной документации Go. Пример из документации: fmt.Println(time.Now().Format(time.RFC3339Nano))

Лайфхак: рекомендую для метода Format при определении времени использовать не строку, а константу с необходимым форматом вывода времени, или несколько таких констант.

Лайфхак сделает код лаконичнее:

Использование константы
Использование константы

Ещё хочу поделиться наблюдением. В предыдущем посте я рассказывал, что ОЗУ работает шустрее внутреннего хранилища (HDD/SSD). С помощью пакета time, мы можем понаблюдать - сколько времени занимают операции с файлами: создание, запись, чтение, изменение уровня доступа.

Посмотрим длительность создания файла и записи строки:

-8

Мы создали файл за 58 миллисекунд, а записали строку в файл за 1 миллисекунду. Эти значения колеблются: когда я запускал этот код несколько раз подряд, получал значения от 45 до 75 миллисекунд на создание файла.

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

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

Разница между датами

В предыдущем примере, для определения длительности создания файла и записи строки, я находил разницу между датами (временем) этих операций за счёт вычитания одного значения из другого в уме.

Можно автоматизировать код за счёт метода sub(), чтобы вычитание отображалось в терминале:

Определение длительности операция методом sub()
Определение длительности операция методом sub()

Вывод в терминал будет следующим:

Вывод в терминал
Вывод в терминал

Что мы видим - файл создан за 1 миллисекунду, а строка записана практически мгновенно.

1 секунда = 1000 миллисекунд. 1 миллисекунда = 0, 001 секунды.

Почему такая разница (в предыдущем примере - за 58 миллисекунд) - не пойму. Но факт есть факт. Причём, перезапуск кода не влияет на вывод значений в терминал: они всегда идентичны. Буду считать, что в IDE на создание файла требуется 1 миллисекунда.

Интересно ещё запустить код в песочнице Replit, т.к. там выше точность отображения длительности операций:

Результат кода в Replit
Результат кода в Replit

Мы видим, что файл создан за 154,66 микросекунды, а строка записана за 27,84 микросекунды. 154 микросекунды = 0, 000 154 секунды, т.е. намного быстрее, чем в IDE на компьютере.

µs - микросекунда. Одна миллисекунда = 1000 микросекунд.

Что тут можно сказать, видимо, в песочнице файл создаётся другим способом, по сравнению с созданием файла в IDE. А ещё, Вариативность длительности операций при перезапуске кода, примерно в диапазоне 40%.

Давайте попробуем посчитать время выполнения других операций.

Чтение из файла

Чтобы понятно было содержимое файла, пропишу его создание и запись строки:

package main

import (
"fmt"
"os"
"time"
)

func main() {
fileName := "testfile"
file, err := os.Create(fileName)
if err != nil {
fmt.Println("Не удалось создать файл")
return
}
defer file.Close()

file.WriteString("TextTextText")

if file, err = os.Open(fileName); err != nil {
fmt.Println("Не удалось открыть файл для чтения")
return
}
defer file.Close()

info, err := file.Stat()
if err != nil {
panic(err)
}

buf := make([]byte, info.Size())
timeStartReading := time.Now()
if _, err = file.Read(buf); err != nil {
fmt.Println("Не удалось прочитать последовательность байт в файле")
return
}
timeEndReading := time.Now()
fmt.Println(string(buf))
durationReading := timeEndReading.Sub(timeStartReading)
fmt.Println("Длительность чтения данных из файла:", durationReading)
}

Вывод времени кода в таком формате, в моей LiteIDE, отображается в миллисекундах. Для этого кода длительность чтения из файла равна нулю.

Пока не разбираюсь с настройками IDE, можно ли изменить эту точность, заданную по-умолчанию. А выполню тест в песочнице Replit, поскольку она выдаёт большую точность. Результат в Replit:

Время чтения данных
Время чтения данных

Итак, данные были прочитаны из файла за 2,54 микросекунды = 0,00000254 секунды. Шустро.

Интересный факт: если изначально в файл записать не просто TextTextText, а, скажем, в тридцать раз больше информации - время чтения плюс-минус не меняется.

Несколько пояснений по коду:

  • При объявлении массива для чтения в буфер, я использовал размер среза, равный размеру содержимого этого файла в байтах.
  • Для определения размера содержимого файла, я использовал info.Stat(). Вместо этой величины, мы могли бы ввести любое число, например 128. Тогда в терминал бы вывелись дополнительные пробелы.
  • Паника прервет выполнение программы, и в примере ниже, действие до return никогда не дойдет (в данном случае return избыточен). Более того отсутствие файла или ошибка ввода вывода вполне возможная ситуация, это не повод для паники.
if err != nil {
panic(err)
return
}

Я такую конструкцию не использовал, просто полезно это знать. Использовал подобную конструкцию для обработки ошибки метода Stat, т.к. пока не знаю что лучше в нём использовать.

  • Если переменная уже объявлена, лаконичнее использовать такую такую конструкцию:
if file, err = os.Open(fileName); err != nil {
fmt.Println("Не удалось открыть файл для чтения")
return
}

А не такую:

file, err = os.Open(fileName)
if err != nil {
fmt.Println("Не удалось открыть файл для чтения")
return
}

Другой метод чтения

Заменим последние строки кода с file.Read(buf) на io.ReadFull(file, buf):

if _, err = io.ReadFull(file, buf); err != nil {
fmt.Println("Не удалось прочитать последовательность байт в файле")
return
}

Результат кода в Replit показывает увеличение длительности чтения в среднем на 15% по сравнению с file.Read(buf), а в некоторых случаях превышает 5 микросекунд против 2,5.

Результат кода
Результат кода

Вывод - метод file.Read(buf) предпочтительнее io.ReadFull(file, buf) исходя из показателя времени чтения.

Время на изменение прав доступа

Уровни доступа рассмотрю в другой статье, сейчас интересно посмотреть - сколько времени на это расходуется. В коде ниже я создаю файл и назначаю ему уровень доступа "только чтение":

package main

import (
"fmt"
"os"
"time"
)

func main() {
fileName := "testfile.txt"
fmt.Println("Создаем файл")
file, err := os.Create(fileName)
if err != nil {
fmt.Println(err)
}
defer file.Close()
timeStartChangeMode := time.Now()
fmt.Println("Изменяем права доступа на режим 'чтение'")
if err = os.Chmod(fileName, 0444); err != nil {
panic(err)
return
}
timeEndChangeMode := time.Now()
timeDurationCangeMode := timeEndChangeMode.Sub(timeStartChangeMode)
fmt.Println("Права доступа изменялись в течение", timeDurationCangeMode)
}

Вывод в терминал будет следующим в песочнице Replit:

Время на изменение прав доступа
Время на изменение прав доступа

Итак, изменение прав доступа занимает 82,37 микросекунды = 0,00008237 секунды.

Лайфхак форматирования

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

package main

import (
"fmt"
"os"
"time"
)

const timeTemplate = "01-02-2006 15:04:05"

func main() {
file, err := os.Create("Log.txt")
if err != nil {
panic(err)
return
}
defer file.Close()

fmt.Println("Введите запрос:")
var answer string
fmt.Scan(&answer)
moment := time.Now().Format(timeTemplate)
line := fmt.Sprintf("%s %s\n", moment, answer)
file.WriteString(line)
}

Будет сделана примерно такая запись в файл:

Слева - содержимое файла, справа - вывод в терминал
Слева - содержимое файла, справа - вывод в терминал

Что интересно в этом коде? Следующие строки:

Фрагмент кода
Фрагмент кода

В этом фрагменте мы подготовили строку для записи в файл. Без этой подготовки, запись в файл того же содержимого могла выглядеть так:

Фрагмент кода
Фрагмент кода

Здесь меньшее число строк, но количество строк - не показатель качества кода. В данном случае лаконичнее первый вариант.

Итоги

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

Метод file.Read(buf) предпочтительнее io.ReadFull(file, buf) исходя из показателя времени чтения из файла.

Основные величины длительности работы с файлами:

  • Создание файла - 150 микросекунд - 1 миллисекунда (в IDE);
  • Запись строки в файл - 30 микросекунд;
  • Чтение из файла - 2,5 микросекунды;
  • Изменение уровня доступа к файлу - 80 микросекунд.

Самая шустрая операция - чтение из файла. Самая медленная - создание файла. Приняли к сведению, запомнили.

--//--//--

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

 NASA https://unsplash.com/photos/2W-QWAC0mzI
NASA https://unsplash.com/photos/2W-QWAC0mzI

Бро, ты уже здесь? 👉 Подпишись на канал для новичков «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻