Содержание урока · 10 разделов
Сценарий для старта: в сервисе отчётов задача «раз в сутки» была сделана на time.NewTicker с интервалом в двадцать четыре часа. Год всё работало, пока команду не перевели на новый деплой: контейнер стал перезапускаться днём после выката релиза, и отчёт сдвинулся с шести утра на четырнадцать часов дня — ровно на момент старта процесса. Второй инцидент в соседней команде: накопитель метрик на тикере с секундным интервалом завис при медленной базе, а тики продолжали сыпаться — никто не заметил, что половина из них теряется мимо занятого потребителя. Оба инцидента — не ошибка кода, а подмена инструмента: в первом случае нужен был календарный механизм, во втором — контроль перекрытий. Дальше — контракты, которые отличают эти инструменты.
time.Ticker: тикер как планировщик#
time.Ticker в Go — это канал, в который runtime отправляет значение каждые T с момента создания: равные интервалы от старта, без привязки к часам суток. Канал тикера буферизован на один элемент — второй тик, пришедший к непрочитанному первому, отбрасывается. Это осознанное решение: тикер — механизм уведомлений «пора смотреть», а не очередь заданий. Пара правил сопровождает каждый тикер: останавливать через Stop, когда он больше не нужен, — из урока про утечки горутин видно, что случается с забытыми тикерами, — и читать канал в одном месте, потому что два читателя делят тики между собой. Устройство каналов и select разбирает урок про каналы; здесь важно, что цикл с select по тикеру и отмене контекста — стандартный каркас воркера на равных интервалах.
Воркер на тикере с отменой
GOfunc 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, где интервал отмеряется от конца работы. Этот шаблон называют равными интервалами с задержкой: работа занимает сколько-то секунд, и следующий отсчёт стартует после неё. Цена та же, что и у дрейфа, — привязка к моменту старта: ни тикер, ни таймер не знают про шесть утра.
Равные интервалы без потери тиков
GOfunc 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.Ticker | cron | time.Timer |
|---|---|---|---|
| момент запуска | равные интервалы от старта | календарное расписание | через N от события |
| привязка к часам суток | нет | есть: час, день, пояс | нет |
| конфиг для человека | число в коде | строка crontab в конфиге | длительность |
| перекрытие запусков | тики теряются, разруливать самому | обёртки SkipIfStillRunning | не бывает |
| смена без релиза | нет | конфиг и перезагрузка | нет |
| типичные задачи | heartbeat, метрики, батчи | отчёты, бэкапы, рассылки | напоминания, отложенные повторы |
Спорное: тикер вместо cron в маленьком сервисе#
Спорная привычка, которую стоит оспорить: в маленьком сервисе тикер проще, а cron — лишняя зависимость. Аргумент честный: стандартная библиотека не требует go get, а строка расписания — мини-язык, который приходится объяснять коллегам. Но цена тикера в календарных задачах выше, чем выглядит: привязка к моменту старта, потеря расписания при рестарте, отсутствие человеческого формата в конфиге и ручное разруливание перекрытий — это строки кода, которые пишутся поверх тикера и повторяют cron хуже. Верное чтение границы: тикер — выбор по природе задачи, а не по размеру проекта. Равные интервалы без календарной привязки, высокая частота, работа «сразу после старта и с той же частотой» — территория тикера. Привязка к часу, дню недели или поясу, изменение частоты без релиза, задачи, о которых говорит конфиг, — территория cron, и размер сервиса на выбор не влияет.
Кейс из первого абзаца, разобранный до конца: тикер с интервалом 24h стартовал вместе с процессом, поэтому отчёт выходил в момент запуска сервиса. Месяцами совпадение было удачным, затем деплой переехал на дневное окно — отчёты ушли на середину дня, а NTP-коррекция часов на хосте добавила редкие сдвиги на минуты. Симптом-подсказка: задача «сдвигается вместе с рестартами» — верный признак тикера там, где нужен календарь. Лечение: строка выражения в конфиге и планировщик из урока про знакомство; расписание становится видимым, редактируемым и переживает рестарты без сдвига.
Чеклист выбора механизма#
- Равные интервалы с высокой частотой и без календарной привязки — time.Ticker.
- Календарное расписание с привязкой к часу, дню и поясу — cron со строкой в конфиге.
- Одноразовый отсчёт от события — time.NewTimer или time.AfterFunc.
- Тикер и таймер создаются с defer Stop на соседней строке.
- Перекрытия тикера разруливаются явно: флаг занятости, обёртка или шаблон с Reset.
- Изменение расписания без релиза возможно только через конфиг планировщика.
Соберите цикл без потери тиков
Впишите остановку таймера, чтение его канала и перезапуск после работы.
Расставьте каркас тикерного воркера
Расставьте строки в порядке файла: создание тикера, освобождение, цикл, выбор, чтение тика и работа.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: тикер, таймер, cron
Это самый дружелюбный песочнице урок: оба предикта исполняются там как есть, потому что пакет time доступен целиком — тикеры, таймеры, len канала и Sleep с миллисекундными интервалами. Уменьшите интервалы до сотен миллисекунд и воспроизведите оба инцидента: потерю тиков занятым потребителем и перезапуск по Reset. Воркеры с контекстом и каналами тоже проверяются на месте — select и close работают честно. Живой cron-планировщик и перезагрузка конфига исполняются локально из модуля курса.
Выбор механизма фоновой работы — чтение контракта, а не привычка: тикер отмеряет равные интервалы от старта и теряет тики занятого потребителя, таймер отсчитывает от события, cron привязывает работу к календарю с поясами и живёт в конфиге. Признаки перепутанных ролей легко ловятся: расписание, сдвигающееся с деплоем, и секундные метки в строках выражений. Дальше по маршруту: итоговый проект — сборка сервиса задач, где оба мира встречаются: календарные отчёты и минутные напоминания в одном планировщике. Если возвращались с пропуска — пакет time даёт полную арифметику таймеров, синтаксис выражений — словарь расписаний, а секунды и NewWithSeconds показывают, когда cron спускается до секундных интервалов. Полный маршрут — на странице курса. В домашнем задании — ревизия своих таймеров по чеклисту.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Тикер отмеряет равные интервалы от момента создания: рестарт сдвигает метки, календаря у него нет.
- Канал тикера буферизован на один тик: занятый потребитель теряет тики, а не копит их.
- Шаблон с NewTimer и Reset убирает потери, но интервал дрейфует вместе с длительностью работы.
- Таймер — один отсчёт от события: напоминания, отложенные повторы, Stop и Reset как часть контракта.
- cron даёт календарную привязку, пояса, человеческий конфиг и смену расписания без релиза.
- Выбор по природе задачи: интервалы — тикер, календарь — cron, отсчёт от события — таймер.
Домашнее задание
Проведите ревизию фоновых механизмов своего проекта: выпишите каждый тикер, таймер и задачу по расписанию и классифицируйте по таблице урока — интервал, календарь, отсчёт от события. Найдите календарные задачи, сделанные на тикере, и переведите их на cron со строкой в конфиге; для проверки сдвига отметьте момент старта процесса и сравните с моментом задачи. Для одного тикера постройте шаблон с Reset и измерьте фактическую частоту под нагрузкой, где работа длиннее интервала: убедитесь в потере тиков у тикера и их сохранении у шаблона. Финал — добавьте к тикерному воркеру ветку отмены по контексту и убедитесь, что Stop вызывается на выходе.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему тики тикера теряются, а не ждут в очереди?
Канал тикера буферизован ровно на один элемент: новый тик при занятом канале отбрасывается. Это решение стандартной библиотеки под контракт уведомлений: потребитель хочет знать, что пора работать, а не получить очередь из ста пропущенных меток, которые устарели ещё при прибытии. Нужна очередь — она строится явно, отдельной структурой.
Можно ли тикером делать задачу «раз в сутки в шесть утра»?
Можно, но контракт будет неверным: интервал в двадцать четыре часа привязан к моменту создания тикера, и при каждом рестарте задачи метка сдвигается. Часовой пояс и перевод часов тоже остаются вне контракта. Для календарной привязки нужен планировщик со строкой выражения — тогда шесть утра остаётся шестью утра независимо от деплоев.
Когда тикер действительно лучше планировщика?
Когда задача по природе равных интервалов: heartbeat и здоровье, сбор метрик, батч-выгрузки, опрос по частоте в десятки герц. Календарная привязка там бессмысленна, человеческий конфиг не нужен, а накладные расходы планировщика на частых тиках были бы заметны. Тикер — стандартная библиотека без зависимостей, и в этих сценариях это весомый аргумент.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь