JavaRush /Курсы /Go SELF /time.Time и time.Duration: моменты, интервалы

time.Time и time.Duration: моменты, интервалы

Go SELF
34 уровень , 0 лекция
Открыта

1. Два смысла слова «время»: момент и длительность

Когда программист говорит «время», он часто имеет в виду сразу два разных смысла. Первый — это конкретный момент: «сейчас», «2026-01-16 10:30». Второй — это длительность: «подожди 2 секунды», «операция заняла 150 миллисекунд».

Если эти смыслы смешать в один тип, ошибки становятся очень… творческими. Go решает проблему заранее: момент времени хранится в time.Time, а длительность — в time.Duration.

Представьте, что time.Time — это точка на линейке, а time.Duration — это отрезок. Точку можно сдвинуть на отрезок и получить новую точку. А вот «сложить две точки» — бессмыслица, поэтому компилятор не даст вам сделать глупость (или хотя бы заставит очень постараться).

Небольшая табличка для фиксации смысла:

Что хотим выразить Тип в Go Пример в реальности Пример в коде
Момент (точка во времени)
time.Time
«сейчас», «в 12:00»
time.Now()
Длительность (интервал)
time.Duration
«2 секунды», «15 минут»
2 * time.Second

2. time.Time: момент времени

Если вы раньше работали с датами как со строками («2026-01-16»), то вы уже знаете, насколько это удобно… пока не нужно сравнить даты, прибавить день или разобраться с часовыми поясами. Тип time.Time — это нормальное, структурированное представление момента времени.

Его можно сравнивать, форматировать, вычитать из него другой момент и получать длительность, и вообще жить без вечного ручного парсинга строк. Строка хороша для вывода или обмена, но плохо подходит как «источник истины» внутри программы.

Самый частый способ получить time.Time — вызвать time.Now(). Это «текущий момент» по системным часам. С ним удобно делать измерения длительности, логировать «когда началось», хранить «когда создано» и так далее.

Мини‑пример: печатаем time.Time и смотрим тип

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()

	fmt.Printf("now=%v\n", now)
	fmt.Printf("type=%T\n", now) // type=time.Time
}

Обратите внимание: fmt умеет печатать time.Time «по-человечески», но это ещё не «контролируемый формат». Сейчас важнее зафиксировать: now — это не строка, а отдельный тип.

Мини‑пример: создаём момент времени вручную

Иногда полезно уметь создавать фиксированный момент. Например, в тестах или в демонстрациях. Для этого есть time.Date. Чтобы не углубляться в зоны и локали, возьмём time.UTC.

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Date(2026, 1, 16, 10, 30, 0, 0, time.UTC)
	fmt.Println(t) // 2026-01-16 10:30:00 +0000 UTC
}

Zero value у time.Time: как Go выражает «время не задано»

У многих типов в Go есть осмысленное значение по умолчанию. Для int это 0, для string это "", для bool это false. У time.Time тоже есть zero value — это «нулевой момент времени».

В реальной жизни он почти никогда не означает «реальную дату», он означает «значение не задано». Чтобы проверить «задано ли время», обычно используют t.IsZero().

package main

import (
	"fmt"
	"time"
)

func main() {
	var t time.Time

	fmt.Println(t.IsZero()) // true
	fmt.Println(t)          // 0001-01-01 00:00:00 +0000 UTC
}

Важно: «0001 год» может выглядеть как «какая-то дата», но в большинстве бизнес‑сценариев это именно маркер «пусто». И это очень удобно для моделей данных.

Пример: задача с DoneAt и проверкой через IsZero()

Давайте начнём аккуратно развивать условное приложение‑трекер задач (пока без файлов, HTTP и базы — только данные). Логика простая: если DoneAt нулевое — задача не завершена.

package main

import (
	"fmt"
	"time"
)

type Task struct {
	Title  string
	DoneAt time.Time
}

func main() {
	t := Task{Title: "прочитать про time.Time"}
	fmt.Println(t.DoneAt.IsZero()) // true
}

3. time.Duration: длительность, единицы и ловушка новичка

time.Duration — это длительность: 10 миллисекунд, 2 секунды, 3 минуты, 1 час. Внутри это число наносекунд, но вы почти никогда не должны думать «наносекундами».

Вместо этого Go даёт константы‑единицы: time.Millisecond, time.Second, time.Minute, time.Hour. Правильный стиль почти всегда выглядит как «число * единица».

Мини‑пример: правильная длительность (и печать)

package main

import (
	"fmt"
	"time"
)

func main() {
	d := 150 * time.Millisecond

	fmt.Println(d)        // 150ms
	fmt.Printf("%T\n", d) // time.Duration
}

Самый классический баг новичка: попытка написать «5 секунд» как time.Duration(5). Это не 5 секунд — это 5 наносекунд. То есть примерно «моргнули — и уже прошло».

Мини‑пример: ловушка time.Duration(5)

package main

import (
	"fmt"
	"time"
)

func main() {
	a := time.Duration(5)
	b := 5 * time.Second

	fmt.Println(a) // 5ns
	fmt.Println(b) // 5s
}

Эту ловушку стоит запомнить как «проверку на внимательность»: если видите time.Duration(число) — почти наверняка там ошибка (или очень редкий случай, где автор реально хотел наносекунды и готов за это отвечать).

4. Как time.Time и time.Duration складываются в «правильную математику»

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

Сдвиг момента делается через Add. Разница моментов — через Sub. И здесь важно: Sub возвращает time.Duration, а не число. То есть Go заставляет вас не забывать единицы измерения.

Мини‑пример: Add и Sub

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	end := start.Add(2 * time.Second)
	elapsed := end.Sub(start)

	fmt.Println(elapsed) // 2s
}

Если вы попробуете сложить два time.Time (например, start + end), компилятор вас остановит. Это полезная «защита от творческого подхода к математике».

Небольшая схема смысла

time.Time      — точка:    ●
time.Duration  — отрезок:  ─────
t.Add(d)       — сдвиг:    ● + ───── = ●
t2.Sub(t1)     — разница:  ● - ● = ─────

5. Монотонное время: зачем оно нужно при замерах

Кажется, что измерить длительность просто: взяли start := time.Now(), потом снова time.Now(), вычли и получили «сколько прошло». Но в реальности системные часы могут сдвигаться: NTP‑синхронизация, администратор руками поправил время, leap second, виртуальная машина мигрировала, да и просто ОС решила «я лучше знаю, сколько сейчас».

Для календаря это нормально, а вот для измерения длительности — неприятно.

Именно поэтому в Go есть концепт монотонной составляющей времени: это внутренний счётчик, который не зависит от того, как показывают «настоящие» часы. Начиная с Go 1.9 пакет time прозрачно отслеживает монотонное время внутри значений Time, и вычисление длительности между двумя Time становится устойчивым к подстройке системных часов.

В частности, классический паттерн start := time.Now(); ...; elapsed := time.Since(start) работает корректно даже при таких сдвигах.

Важно понимать границу: монотонная часть нужна для измерения длительностей, а «настенное время» (wall clock) — для того, чтобы печатать даты, хранить «создано в 10:30» и показывать пользователю.

Мини‑пример: замер длительности через time.Since

package main

import (
	"fmt"
	"time"
)

func main() {
	start := time.Now()
	time.Sleep(20 * time.Millisecond)

	fmt.Println(time.Since(start)) // ~20ms (плюс-минус)
}

Почему мы так любим time.Since(start), а не time.Now().Sub(start)? Потому что оно читается как фраза («прошло с момента start») и является идиоматичным способом замеров.

Ещё одна полезная интуиция: монотонная составляющая не про «точную дату», а про «сколько прошло». Поэтому, когда вы форматируете time.Time в строку или сохраняете его как текст, монотонная часть не сохраняется. Это не баг — это логика: строка «2026-01-16 10:30» не должна содержать скрытый счётчик из процессора.

Мини‑кусочек приложения: CreatedAt и «возраст» задачи

В реальных приложениях очень часто нужно хранить «когда объект создан». Для задач это выглядит естественно: CreatedAt time.Time. Тогда мы можем показать пользователю «задача создана 3 минуты назад» или «задача висит уже 2 дня».

Сделаем маленькое расширение модели Task: добавим CreatedAt и посчитаем длительность «сколько прошло с создания». Обратите внимание, что мы возвращаем и печатаем именно time.Duration, а не int, чтобы не терять смысл единиц измерения.

package main

import (
	"fmt"
	"time"
)

type Task struct {
	Title     string
	CreatedAt time.Time
}

func main() {
	t := Task{Title: "разобраться со временем", CreatedAt: time.Now()}
	time.Sleep(10 * time.Millisecond)

	fmt.Println(time.Since(t.CreatedAt)) // ~10ms
}

Теперь добавим DoneAt и аккуратную проверку «задача завершена или нет» через IsZero(). Это будет выглядеть довольно по‑взрослому уже сейчас, даже без базы данных и REST API.

package main

import "time"

type Task struct {
	Title     string
	CreatedAt time.Time
	DoneAt    time.Time
}

func (t Task) IsDone() bool {
	return !t.DoneAt.IsZero()
}

Здесь важно, что IsZero() — ваш лучший друг в модели. Он намного понятнее, чем сравнивать t.DoneAt == time.Time{} (так тоже можно, но читать это тяжелее, особенно новичкам).

6. Типичные ошибки при работе с time.Time и time.Duration

Ошибка №1: писать time.Duration(5) и ожидать «5 секунд».
Это одна из самых распространённых проблем, потому что код выглядит правдоподобно. На самом деле это 5 наносекунд. Правильная форма почти всегда: 5 * time.Second, 250 * time.Millisecond, 3 * time.Minute. Если видите time.Duration(число) — перепроверьте, действительно ли автор хотел наносекунды.

Ошибка №2: хранить моменты времени строками «потому что так проще».
Пока вы только печатаете дату — кажется, что проще. Но как только нужно сравнить, прибавить интервал, вычислить разницу или проверить «задано ли значение», строки превращаются в источник боли. Храните момент как time.Time, а строку делайте только для вывода или обмена (с чётким форматом).

Ошибка №3: пытаться «прибавить два момента» или «вычесть длительность из длительности» не тем типом.
Если вы видите желание сделать что-то вроде t1 + t2, остановитесь и спросите себя: «что это значит в реальности?» Момент + момент не имеет смысла. Момент + длительность имеет. Длительность + длительность имеет. Разница моментов — длительность.

Ошибка №4: не использовать IsZero() и воспринимать 0001-01-01... как настоящую дату.
Печать zero value у time.Time выглядит как дата, и новичок может начать обрабатывать её как «очень древнюю задачу». Обычно это именно «не задано». В моделях данных привычка «время может отсутствовать» отлично выражается через zero value + IsZero().

Ошибка №5: измерять длительность «настенными» часами и удивляться странным результатам.
Если вы вручную делаете end := time.Now(); elapsed := end.Sub(start), это обычно нормально. Но если системное время будет подстроено, можно получить неожиданные эффекты. Идиоматичный путь замеров — start := time.Now(); ...; elapsed := time.Since(start). Этот паттерн как раз поддерживает идею монотонного времени и задуман как устойчивый к корректировкам часов.

1
Задача
Go SELF, 34 уровень, 0 лекция
Недоступна
Смена расписания
Смена расписания
1
Задача
Go SELF, 34 уровень, 0 лекция
Недоступна
Таймер новичка
Таймер новичка
1
Задача
Go SELF, 34 уровень, 0 лекция
Недоступна
Статус задачи
Статус задачи
1
Задача
Go SELF, 34 уровень, 0 лекция
Недоступна
Проверка скорости
Проверка скорости
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ