1. Два смысла слова «время»: момент и длительность
Когда программист говорит «время», он часто имеет в виду сразу два разных смысла. Первый — это конкретный момент: «сейчас», «2026-01-16 10:30». Второй — это длительность: «подожди 2 секунды», «операция заняла 150 миллисекунд».
Если эти смыслы смешать в один тип, ошибки становятся очень… творческими. Go решает проблему заранее: момент времени хранится в time.Time, а длительность — в time.Duration.
Представьте, что time.Time — это точка на линейке, а time.Duration — это отрезок. Точку можно сдвинуть на отрезок и получить новую точку. А вот «сложить две точки» — бессмыслица, поэтому компилятор не даст вам сделать глупость (или хотя бы заставит очень постараться).
Небольшая табличка для фиксации смысла:
| Что хотим выразить | Тип в Go | Пример в реальности | Пример в коде |
|---|---|---|---|
| Момент (точка во времени) | |
«сейчас», «в 12:00» | |
| Длительность (интервал) | |
«2 секунды», «15 минут» | |
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). Этот паттерн как раз поддерживает идею монотонного времени и задуман как устойчивый к корректировкам часов.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ