Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 24 из 25 ~16 мин +20 XP

cron против time.Ticker и NewTimer

Выбор между time ticker go и cron в Go-сервисе путают реже, чем стоило бы: тикер и таймер есть в стандартной библиотеке без зависимостей, поэтому в маленьком проекте им отдают всё, а потом задачу «отчёт в девять утра» привязывают к моменту запуска процесса. Обратная крайность — cron для heartbeat раз в секунду, где строка выражения ничего не добавляет, а планировщик тратит память впустую. Инструменты решают разные задачи: тикер отмеряет равные интервалы, таймер — один отсчёт, cron привязывает работу к календарю с часовыми поясами и человеческим конфигом. Разберём контракты всех трёх, арифметику дрейфа и перекрытий и критерии, по которым выбирается механизм для конкретной задачи.

Содержание урока · 10 разделов

Сценарий для старта: в сервисе отчётов задача «раз в сутки» была сделана на time.NewTicker с интервалом в двадцать четыре часа. Год всё работало, пока команду не перевели на новый деплой: контейнер стал перезапускаться днём после выката релиза, и отчёт сдвинулся с шести утра на четырнадцать часов дня — ровно на момент старта процесса. Второй инцидент в соседней команде: накопитель метрик на тикере с секундным интервалом завис при медленной базе, а тики продолжали сыпаться — никто не заметил, что половина из них теряется мимо занятого потребителя. Оба инцидента — не ошибка кода, а подмена инструмента: в первом случае нужен был календарный механизм, во втором — контроль перекрытий. Дальше — контракты, которые отличают эти инструменты.

time.Ticker: тикер как планировщик#

time.Ticker в Go — это канал, в который runtime отправляет значение каждые T с момента создания: равные интервалы от старта, без привязки к часам суток. Канал тикера буферизован на один элемент — второй тик, пришедший к непрочитанному первому, отбрасывается. Это осознанное решение: тикер — механизм уведомлений «пора смотреть», а не очередь заданий. Пара правил сопровождает каждый тикер: останавливать через Stop, когда он больше не нужен, — из урока про утечки горутин видно, что случается с забытыми тикерами, — и читать канал в одном месте, потому что два читателя делят тики между собой. Устройство каналов и select разбирает урок про каналы; здесь важно, что цикл с select по тикеру и отмене контекста — стандартный каркас воркера на равных интервалах.

Воркер на тикере с отменой

GO
func sweeper(ctx context.Context, interval time.Duration) {
    ticker := time.NewTicker(interval)
    defer ticker.Stop()

    for {
        select {
        case <-ticker.C:
            sweep() // чистка устаревших сессий
        case <-ctx.Done():
            return
        }
    }
}

Каркас тикерного воркера: создание, отложенный Stop, цикл select из двух веток. Ветка тика выполняет работу, ветка отмены закрывает воркер вместе с сервисом — контракт из урока про контекст. Интервал здесь — величина из кода: чтобы изменить частоту, нужен релиз, и это первый пункт в сравнении с cron.

Проверка: тики при медленном потребителе

Потребитель обрабатывает каждый тик пять секунд, интервал тикера — одна секунда. Что происходит с тиками между завершениями обработки?

Дрейф и перекрытие: арифметика тикера#

Тикер отмеряет интервалы от своего старта, и этот факт порождает два следствия. Первое — дрейф: рестарт процесса сдвигает все последующие тики на момент рестарта, и «раз в сутки» оказывается «раз в сутки от запуска». Второе — перекрытие: если работа длится дольше интервала, потребитель не успевает, тики теряются, и фактическая частота падает до скорости обработки. Лечится пара по-разному: перекрытие — флагом занятости или обёрткой с пропуском, а желание «не терять тики» — шаблоном с NewTimer и Reset, где интервал отмеряется от конца работы. Этот шаблон называют равными интервалами с задержкой: работа занимает сколько-то секунд, и следующий отсчёт стартует после неё. Цена та же, что и у дрейфа, — привязка к моменту старта: ни тикер, ни таймер не знают про шесть утра.

Равные интервалы без потери тиков

GO
func poller(ctx context.Context, interval time.Duration) {
    timer := time.NewTimer(interval)
    defer timer.Stop()

    for {
        select {
        case <-ctx.Done():
            return
        case <-timer.C:
            poll() // работа любой длительности
            timer.Reset(interval) // отсчёт от конца работы
        }
    }
}

Timer срабатывает один раз, после работы перезапускается через Reset: тики не теряются, потому что следующий отсчёт начинается после завершения обработки. Интервал между запусками работы равен сумме времени работы и паузы, поэтому частота дрейфует вместе с длительностью работы. Выбор между тикером и этим шаблоном — выбор между «фиксированные метки, часть тиков можно потерять» и «без потерь, но с дрейфом».

Сколько тиков ждёт в канале?

Прочитайте код и выберите, что он выведет:

ticker := time.NewTicker(time.Second)
defer ticker.Stop()

time.Sleep(3500 * time.Millisecond)

fmt.Println(len(ticker.C))

cron против тикера: разница в контракте#

cron vs ticker — разница не в удобстве, а в контракте. Планировщик привязывает запуск к календарю: минута, час, день недели, пояс — и переживает рестарты, потому что строка расписания хранится в конфиге, а не в памяти процесса. Тикер привязывает запуск к моменту собственного создания и вместе с ним умирает. Смена расписания без релиза доступна только cron: перезагрузка конфига из урока про изменение расписания меняет частоту и время задач, тогда как интервал тикера зашит в бинарник. Взамен тикер даёт простоту и микросекундные накладные расходы: heartbeat, сбор метрик, батч-выгрузки каждые сто миллисекунд — задачи, где календарная привязка бессмысленна, а строка выражения только мешала бы. Часовые пояса и переводы часов — территория планировщика: механика из урока про пояса работает на строках расписания, тикеру про зоны нечего сказать.

NewTimer: одноразовые отсчёты#

time.NewTimer — третий инструмент: один отсчёт до истечения длительности, значение в канал, конец. Родственная time.AfterFunc вызывает функцию по истечении в отдельной горутине. Типичные задачи таймера — «через N после события»: напоминание о броне за час до начала, повторный опрос через пять минут, отсроченная отправка. Это противоположность планировщика: cron отвечает на вопрос «когда по календарю», таймер — «через сколько от события», и событие чаще всего случается вне расписания. Для накопления напоминаний многих пользователей строится карта таймеров или мини-очередь по срокам — механика разобрана в уроке про напоминания бота. Правила гигиены те же: Stop для отменённого таймера, Reset для переиспользования, осознанность с time.After в горячих циклах — из урока про утечки.

Проверка: напоминание через два часа

Пользователь создал событие, напоминание должно прийти через два часа после него. Какой механизм подходит?

Тикер против cron против таймера#

Свойствоtime.Tickercrontime.Timer
момент запускаравные интервалы от стартакалендарное расписаниечерез N от события
привязка к часам сутокнетесть: час, день, пояснет
конфиг для человекачисло в кодестрока crontab в конфигедлительность
перекрытие запусковтики теряются, разруливать самомуобёртки SkipIfStillRunningне бывает
смена без релизанетконфиг и перезагрузканет
типичные задачиheartbeat, метрики, батчиотчёты, бэкапы, рассылкинапоминания, отложенные повторы

Спорное: тикер вместо cron в маленьком сервисе#

Спорная привычка, которую стоит оспорить: в маленьком сервисе тикер проще, а cron — лишняя зависимость. Аргумент честный: стандартная библиотека не требует go get, а строка расписания — мини-язык, который приходится объяснять коллегам. Но цена тикера в календарных задачах выше, чем выглядит: привязка к моменту старта, потеря расписания при рестарте, отсутствие человеческого формата в конфиге и ручное разруливание перекрытий — это строки кода, которые пишутся поверх тикера и повторяют cron хуже. Верное чтение границы: тикер — выбор по природе задачи, а не по размеру проекта. Равные интервалы без календарной привязки, высокая частота, работа «сразу после старта и с той же частотой» — территория тикера. Привязка к часу, дню недели или поясу, изменение частоты без релиза, задачи, о которых говорит конфиг, — территория cron, и размер сервиса на выбор не влияет.

Чеклист выбора механизма#

  • Равные интервалы с высокой частотой и без календарной привязки — time.Ticker.
  • Календарное расписание с привязкой к часу, дню и поясу — cron со строкой в конфиге.
  • Одноразовый отсчёт от события — time.NewTimer или time.AfterFunc.
  • Тикер и таймер создаются с defer Stop на соседней строке.
  • Перекрытия тикера разруливаются явно: флаг занятости, обёртка или шаблон с Reset.
  • Изменение расписания без релиза возможно только через конфиг планировщика.

Соберите цикл без потери тиков

Впишите остановку таймера, чтение его канала и перезапуск после работы.

timer := time.NewTimer(interval) defer timer.() for { select { case <-timer.: work() timer.(interval) case <-ctx.Done(): return } }

Расставьте каркас тикерного воркера

Расставьте строки в порядке файла: создание тикера, освобождение, цикл, выбор, чтение тика и работа.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: тикер, таймер, cron

Термин

time.Ticker

нажмите, чтобы перевернуть
Определение

Канал с тиком каждые T от момента создания. Равные интервалы без привязки к календарю; Stop обязателен, канал буферизован на один тик.

1 / 6

Выбор механизма фоновой работы — чтение контракта, а не привычка: тикер отмеряет равные интервалы от старта и теряет тики занятого потребителя, таймер отсчитывает от события, cron привязывает работу к календарю с поясами и живёт в конфиге. Признаки перепутанных ролей легко ловятся: расписание, сдвигающееся с деплоем, и секундные метки в строках выражений. Дальше по маршруту: итоговый проект — сборка сервиса задач, где оба мира встречаются: календарные отчёты и минутные напоминания в одном планировщике. Если возвращались с пропуска — пакет time даёт полную арифметику таймеров, синтаксис выражений — словарь расписаний, а секунды и NewWithSeconds показывают, когда cron спускается до секундных интервалов. Полный маршрут — на странице курса. В домашнем задании — ревизия своих таймеров по чеклисту.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Тикер отмеряет равные интервалы от момента создания: рестарт сдвигает метки, календаря у него нет.
  • Канал тикера буферизован на один тик: занятый потребитель теряет тики, а не копит их.
  • Шаблон с NewTimer и Reset убирает потери, но интервал дрейфует вместе с длительностью работы.
  • Таймер — один отсчёт от события: напоминания, отложенные повторы, Stop и Reset как часть контракта.
  • cron даёт календарную привязку, пояса, человеческий конфиг и смену расписания без релиза.
  • Выбор по природе задачи: интервалы — тикер, календарь — cron, отсчёт от события — таймер.

Домашнее задание

Проведите ревизию фоновых механизмов своего проекта: выпишите каждый тикер, таймер и задачу по расписанию и классифицируйте по таблице урока — интервал, календарь, отсчёт от события. Найдите календарные задачи, сделанные на тикере, и переведите их на cron со строкой в конфиге; для проверки сдвига отметьте момент старта процесса и сравните с моментом задачи. Для одного тикера постройте шаблон с Reset и измерьте фактическую частоту под нагрузкой, где работа длиннее интервала: убедитесь в потере тиков у тикера и их сохранении у шаблона. Финал — добавьте к тикерному воркеру ветку отмены по контексту и убедитесь, что Stop вызывается на выходе.

Личные заметкиПусто
Хранятся только в вашем браузере (Cron · cron против time.Ticker и NewTimer)

Вопросы и ответы по уроку

Почему тики тикера теряются, а не ждут в очереди?

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

Можно ли тикером делать задачу «раз в сутки в шесть утра»?

Можно, но контракт будет неверным: интервал в двадцать четыре часа привязан к моменту создания тикера, и при каждом рестарте задачи метка сдвигается. Часовой пояс и перевод часов тоже остаются вне контракта. Для календарной привязки нужен планировщик со строкой выражения — тогда шесть утра остаётся шестью утра независимо от деплоев.

Когда тикер действительно лучше планировщика?

Когда задача по природе равных интервалов: heartbeat и здоровье, сбор метрик, батч-выгрузки, опрос по частоте в десятки герц. Календарная привязка там бессмысленна, человеческий конфиг не нужен, а накладные расходы планировщика на частых тиках были бы заметны. Тикер — стандартная библиотека без зависимостей, и в этих сценариях это весомый аргумент.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Slog25 уроков~9 чпрактика25 уроков уже доступны

Логирование в Go: структурные логи на slog

Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.

  • Логировать структурированно: уровни, ключи-атрибуты, группы
  • Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
  • Мигрировать со старого пакета log без переписывания всей кодовой базы
  • Держать производительность: Enabled, предвычисление, горячий путь