Содержание

Время и таймеры в 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-022026-05-23
15:0418:30
2006-01-02 15:04:052026-05-23 18:30:00
time.RFC33392026-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), }

Частые ошибки

  1. Хранить duration как int без единиц измерения.
  2. Парсить локальное время через time.Parse, когда нужен ParseInLocation.
  3. Сравнивать отформатированные строки вместо time.Time.
  4. Забывать ticker.Stop().
  5. Создавать time.After внутри бесконечного цикла без необходимости.
  6. Хранить дату без часового пояса там, где важен точный момент.

Чеклист

  1. Для момента времени используйте time.Time.
  2. Для длительностей используйте time.Duration.
  3. Для API выбирайте формат с timezone, например time.RFC3339.
  4. Для пользовательского локального времени используйте ParseInLocation.
  5. Для таймаутов в сервисах используйте context.
  6. Останавливайте Timer и Ticker, когда они больше не нужны.
  7. В тестируемый код передавайте now, если от текущего времени зависит логика.

Следующий шаг после статьи

Закрепите тему во вводном проекте без регистрации, а затем переходите к курсам.

Продолжить изучение

Выбери следующую статью по маршруту или углубись в смежную тему.

Похожие статьи