Время и таймеры в Go
Время встречается почти в каждой программе. CLI измеряет, сколько выполнялась команда. Backend ставит таймаут на запрос к базе. Кэш хранит запись десять минут. Планировщик запускает задачу каждый час. API возвращает дату создания объекта. Логи пишут timestamp, чтобы потом понять последовательность событий.
Пакет time в Go закрывает эти задачи: текущий момент, длительности, форматирование, парсинг, таймеры, тикеры, дедлайны и часовые пояса.
Главная идея: не храните время как случайный int или строку без причины. В Go есть специальные типы time.Time и time.Duration, и они делают код понятнее и безопаснее.
time.Time и текущий момент
time.Time представляет конкретный момент времени.
now := time.Now() fmt.Println(now)
Это не "длительность" и не "дата в календаре", а именно момент на временной шкале.
Для измерения длительности обычно сохраняют старт и используют time.Since:
start := time.Now() runTask() elapsed := time.Since(start) fmt.Println("done in", elapsed)
Так часто измеряют время обработки запроса, генерации отчета или выполнения внешнего API-вызова.
time.Duration
time.Duration хранит длительность. Внутри это число наносекунд, но писать напрямую наносекунды почти никогда не нужно.
timeout := 5 * time.Second interval := 200 * time.Millisecond ttl := 10 * time.Minute
Длительности можно складывать, сравнивать и передавать в функции:
if elapsed > timeout { fmt.Println("слишком долго") }
Это лучше, чем передавать 5000 и гадать, это секунды, миллисекунды или что-то еще.
Форматирование времени
В Go формат времени задается не через %Y-%m-%d, как во многих языках. Go использует эталонную дату:
formatted := time.Now().Format("2006-01-02 15:04:05") fmt.Println(formatted)
Эталонная дата:
Mon Jan 2 15:04:05 MST 2006
Ее удобно запомнить как набор чисел:
01/02 03:04:05PM 2006 -0700
Go смотрит на layout и понимает: где стоит 2006, там должен быть год; где 01, там месяц; где 02, там день.
Частые форматы:
| Формат | Пример |
|---|---|
2006-01-02 | 2026-05-23 |
15:04 | 18:30 |
2006-01-02 15:04:05 | 2026-05-23 18:30:00 |
time.RFC3339 | 2026-05-23T18:30:00+05:00 |
Для API чаще выбирают RFC3339, потому что там есть дата, время и часовой пояс.
Парсинг времени
Парсинг - это обратная операция: строка превращается в time.Time.
value, err := time.Parse("2006-01-02", "2026-05-23") if err != nil { return fmt.Errorf("parse date: %w", err) }
Если строка содержит часовой пояс, time.Parse сможет его учесть:
value, err := time.Parse(time.RFC3339, "2026-05-23T18:30:00+05:00")
Если строка не содержит часовой пояс, time.Parse считает время UTC. Для пользовательского локального времени это часто неправильно.
ParseInLocation
Если пользователь вводит локальное время, укажите location явно:
loc, err := time.LoadLocation("Asia/Almaty") if err != nil { return err } value, err := time.ParseInLocation( "2006-01-02 15:04", "2026-05-23 18:30", loc, ) if err != nil { return err }
UTC и локальное время
В backend-системах часто используют правило:
- внутри системы хранить время в UTC;
- пользователю показывать время в его часовом поясе.
Это уменьшает путаницу, когда пользователи находятся в разных странах.
createdAtUTC := time.Now().UTC() local := createdAtUTC.In(loc)
Если вы храните строку "2026-05-23 18:30" без timezone, через месяц будет сложно понять, что она означала.
Sleep, Timer, Ticker
time.Sleep просто останавливает текущую goroutine на время:
time.Sleep(500 * time.Millisecond)
Для одного события используйте timer:
timer := time.NewTimer(2 * time.Second) defer timer.Stop() <-timer.C fmt.Println("готово")
Для периодической задачи используйте ticker:
ticker := time.NewTicker(time.Second) defer ticker.Stop() for tick := range ticker.C { fmt.Println("tick", tick) }
Ticker нужно останавливать, когда он больше не нужен. Иначе он продолжит держать ресурсы.
Таймаут через select
Каналы времени хорошо работают с select:
select { case result := <-results: fmt.Println(result) case <-time.After(5 * time.Second): fmt.Println("таймаут") }
Этот код ждет либо результат, либо истечение пяти секунд.
Для серверного кода чаще используют context.WithTimeout, потому что context можно передать глубже: в HTTP-запрос, SQL-запрос или другую goroutine.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
Теперь таймаут отменит не только ожидание в текущей функции, но и сам HTTP-запрос.
TTL и кэш
TTL означает time to live - время жизни записи. Пример:
type CacheItem struct { Value string ExpiresAt time.Time } func (item CacheItem) Expired(now time.Time) bool { return now.After(item.ExpiresAt) }
Лучше передавать now аргументом, а не вызывать time.Now() внутри каждой функции. Так код проще тестировать:
item := CacheItem{ Value: "cached response", ExpiresAt: now.Add(10 * time.Minute), }
Частые ошибки
- Хранить duration как
intбез единиц измерения. - Парсить локальное время через
time.Parse, когда нуженParseInLocation. - Сравнивать отформатированные строки вместо
time.Time. - Забывать
ticker.Stop(). - Создавать
time.Afterвнутри бесконечного цикла без необходимости. - Хранить дату без часового пояса там, где важен точный момент.
Чеклист
- Для момента времени используйте
time.Time. - Для длительностей используйте
time.Duration. - Для API выбирайте формат с timezone, например
time.RFC3339. - Для пользовательского локального времени используйте
ParseInLocation. - Для таймаутов в сервисах используйте
context. - Останавливайте
TimerиTicker, когда они больше не нужны. - В тестируемый код передавайте
now, если от текущего времени зависит логика.