Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.
Хой! Джедаи и Амазонки!
Разберём основы форматирования даты и времени. Будет много примеров. Также поделюсь лайфхаками, как сделать код лаконичнее. Go!
Время и дата
Время и дата - важный элемент backend'a, например для ведения log-журнала событий.
Задача: вывести в терминал время выполнения конкретной операции. Пример простейшего кода для вывода даты и времени в терминал:
Программа выдаст примерно следующий результат в IDE:
Либо та же программа, выполненная в песочнице 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 и упростим синтаксис:
Итак, мы получили приемлемый вывод дата-время. Сравните:
Было: 2023-02-20 23:10:12.6852837 +1000 +10 m=+0.001000001;
Стало: 2023-2-20 23:23:12.
Это громоздкий метод, есть попроще.
2. Метод 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
Если вы посмотрите на каждый элемент даты и времени, вы увидите, что они увеличиваются на единицу в каждом последующем элементе:
- Указан месяц 01;
- Указан день месяца 02;
- Указан час 03;
- Минута 04;
- Секунда 05;
- Год - 06 (точнее, 2006);
- Часовой пояс - 07.
Вот такое объяснение появления шаблона.
Больше вариантов, как задавать время с помощью метода Format, рекомендую смотреть в официальной документации Go. Пример из документации: fmt.Println(time.Now().Format(time.RFC3339Nano))
Лайфхак: рекомендую для метода Format при определении времени использовать не строку, а константу с необходимым форматом вывода времени, или несколько таких констант.
Лайфхак сделает код лаконичнее:
Ещё хочу поделиться наблюдением. В предыдущем посте я рассказывал, что ОЗУ работает шустрее внутреннего хранилища (HDD/SSD). С помощью пакета time, мы можем понаблюдать - сколько времени занимают операции с файлами: создание, запись, чтение, изменение уровня доступа.
Посмотрим длительность создания файла и записи строки:
Мы создали файл за 58 миллисекунд, а записали строку в файл за 1 миллисекунду. Эти значения колеблются: когда я запускал этот код несколько раз подряд, получал значения от 45 до 75 миллисекунд на создание файла.
Измерять продолжительность времени между операциями неудобно в таком формате - приходится вручную вычитать. Хотелось бы увидеть не время начала операций, а продолжительность операций.
Если мы хотим автоматизировать процесс определения длительности операций в коде, обычная арифметика не работает. Нужны дополнительные методы.
Разница между датами
В предыдущем примере, для определения длительности создания файла и записи строки, я находил разницу между датами (временем) этих операций за счёт вычитания одного значения из другого в уме.
Можно автоматизировать код за счёт метода sub(), чтобы вычитание отображалось в терминале:
Вывод в терминал будет следующим:
Что мы видим - файл создан за 1 миллисекунду, а строка записана практически мгновенно.
1 секунда = 1000 миллисекунд. 1 миллисекунда = 0, 001 секунды.
Почему такая разница (в предыдущем примере - за 58 миллисекунд) - не пойму. Но факт есть факт. Причём, перезапуск кода не влияет на вывод значений в терминал: они всегда идентичны. Буду считать, что в IDE на создание файла требуется 1 миллисекунда.
Интересно ещё запустить код в песочнице 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, воспользуйтесь моей реферальной ссылкой. Вы получите огромную скидку на курс и плюс в карму за помощь каналу.
Бро, ты уже здесь? 👉 Подпишись на канал для новичков «Войти в IT» в Telegram, будем изучать IT вместе 👨💻👩💻👨💻