Содержание урока · 10 разделов
- Что такое cron в Go: планировщик внутри вашего процесса
- Фоновые задачи go-сервиса: типовой список
- Системный crontab против планировщика в приложении
- Три способа запускать периодические задачи
- Расписание регистрируется при старте сервиса
- Почему не time.Ticker: три ловушки самодельного планировщика
- Карта курса: от первого тика до боевого планировщика
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Типичная история знакомства с планировщиком выглядит так. Сервис-магазин отработал релиз, выкатил первые фичи, и тут бизнес просит «раз в сутки присылать владельцу отчёт по продажам». Раз в сутки — несложно: кто-то предлагает крон в системном crontab, кто-то горутину с time.Sleep в цикле. Через месяц задач уже пять: напомнить неоплатившим корзину, удалить брошенные черновики, синхронизировать остатки с внешним API, перечитать курсы валют, отправить дайджест. У каждой задачи своё время, свои часовые пояса, свои требования к ошибкам — и внезапно оказывается, что пять sleep-горутин в main() больше не поддаются чтению. В экосистеме Go эту боль решает robfig/cron: планировщик с синтаксисом классического cron, который живёт внутри вашего приложения, а не где-то в системе. Весь курс построен на третьей версии библиотеки — v3, потому что именно она умеет часовые пояса, секундные расписания и обёртки задач. Полный маршрут из двадцати пяти уроков лежит на странице курса; здесь — карта местности, чтобы дальше не блуждать.
Что такое cron в Go: планировщик внутри вашего процесса#
Cron в Go — это не отдельный демон и не настройка операционной системы, а обычная библиотека, которую вы импортируете и запускаете в своём процессе. robfig/cron создаёт внутри программы цикл, который раз в минуту просыпается, сверяет текущее время с зарегистрированными расписаниями и запускает те задачи, что подошли. Каждая запущенная задача получает собственную горутину, поэтому одна длинная задача не мешает другим тикать. Отсюда важное следствие: планировщик живёт ровно столько, сколько живёт процесс. Перезапустили приложение — расписание поднялось заново, задачи пошли по новым часам. Упал процесс — задачи замолчали вместе с ним, и это честно: фоновые задачи не обязаны переживать смерть сервиса, который их породил. Такое решение быстрее всего объяснить через то, чего оно даёт: задача по расписанию разделяет с сервисом конфиги, подключение к базе, логгер и правила graceful shutdown, а не договаривается с ним через файлы и переменные окружения.
Фоновые задачи go-сервиса: типовой список#
- Ночные отчёты и дайджесты: раз в сутки собрать агрегаты и отправить письмом или в чат.
- Чистка: удалить просроченные сессии, временные файлы, неотправленные уведомления старше недели.
- Напоминания: пингануть пользователей о неоплаченной корзине через час после брошенного шага.
- Синхронизация: перечитать курсы валют, слить изменения с внешним API каждые N минут.
- Обслуживание: бэкап, ротация логов, пересчёт кэша в часы минимальной нагрузки.
- Мониторинг: раз в пять минут проверять внешние сервисы и слать алерт при деградации.
Системный crontab против планировщика в приложении#
Классический ответ на задачу «запускать по расписанию» — системный crontab: демон cron операционной системы читает файл с расписаниями и запускает указанные процессы целиком. Схема рабочая, но у неё есть цена, которую видно изнутри Go-сервиса. Cron запускает новый процесс: ему нужны бинарник, переменные окружения, доступ к базе — всё это дублируется отдельно от основного приложения, и конфиги начинают жить в двух местах. Процесс не знает о состоянии сервиса: задача может стартовать, пока сервис в деплое, и работать против старой базы. Логи и ошибки задач живут в syslog, отдельно от остального. И главное — расписание статично: поменять его значит отредактировать файл на сервере. Планировщик в приложении переворачивает картину: задача — это функция в том же бинарнике, с тем же конфигом и тем же подключением к базе; расписание читается из кода или конфига и меняется обычным деплоем; ошибки идут в общий логгер. Единственное, за что приходится платить, — задачи умирают вместе с процессом и дублируются, если сервис масштабируется в несколько инстансов: про это отдельный урок про блокировки при нескольких инстансах.
Три способа запускать периодические задачи#
| Способ | Где живёт | Сильная сторона | Типовая проблема |
|---|---|---|---|
| Системный crontab | демон ОС, вне приложения | переживает рестарт сервиса, ничего не стоит | дублирование конфигов, задачи оторваны от кода |
| robfig/cron v3 | горутина внутри процесса | общий конфиг, БД, логи; расписание меняется деплоем | умирает с процессом, дубли при нескольких инстансах |
| time.Ticker в цикле | своя горутина, своё решение | нет зависимости, полный контроль | дрейф времени, нет парсинга расписаний и управления задачами |
Первый взгляд: robfig/cron за пятнадцать строк
GOpackage main
import (
"fmt"
"log"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
_, err := c.AddFunc("*/5 * * * *", func() {
fmt.Println("проверка сервера раз в пять минут")
})
if err != nil {
log.Fatalf("выражение не разобрано: %v", err)
}
c.Start()
fmt.Println("планировщик запущен")
select {} // процесс живёт — планировщик тоже
}Не разбирайте пока каждую строку — это телепорт вперёд, установка и подробный разбор приходят в следующем уроке. cron.New создаёт планировщик, AddFunc регистрирует функцию с расписанием «каждые пять минут» и сразу проверяет выражение, Start запускает цикл планировщика в фоне. Строка select {} держит main живым: без неё программа завершится мгновенно и тик не наступит никогда — это самая частая ошибка первого дня.
Расписание регистрируется при старте сервиса#
Практическая деталь, которая отличает зрелую связку от учебной: регистрация задач — это код инициализации, который выполняется один раз при старте приложения. Планировщик собрался, задачи описали рядом с остальными зависимостями — рядом с конфигом, логгером, подключением к базе — и вызвали Start. Дальше приложение живёт своей жизнью: принимает HTTP-запросы, обрабатывает сообщения, а планировщик в фоне следит за временем. Заметьте, что AddFunc можно вызывать и после Start — планировщик это разрешает, и это пригодится, когда расписание будет приходить из базы или конфига и меняться без рестарта. Но базовый паттерн один: всё расписание объявлено в одном месте, видно с первого экрана main.go, и по нему сразу понятно, что сервис делает каждые пять минут и что — раз в сутки. Когда задач становится больше десяти, единый список превращается в документацию сервиса лучше любого вики-раздела.
Три типовых расписания одной строкой
GO// ночной отчёт: каждый день в 03:00
c.AddFunc("0 3 * * *", sendDailyReport)
// чистка сессий: каждые пятнадцать минут
c.AddFunc("*/15 * * * *", cleanupSessions)
// синхронизация: раз в полчаса без привязки к часам
c.AddFunc("@every 30m", syncCurrencyRates)Это меню, а не программа: три типовых расписания, которые покрывают большинство реальных задач. Первое выражение — классические пять полей: в 03:00 каждого дня. Второе использует шаг со звёздочкой — каждые пятнадцать минут по сетке. Третье — дескриптор @every из robfig/cron: интервал от момента запуска, без привязки к круглым часам. Синтаксис выражений подробно разбирается в третьем и четвёртом уроках.
Почему не time.Ticker: три ловушки самодельного планировщика#
Вопрос «зачем библиотека, если есть time.Ticker» всплывает в каждом обсуждении, и честный ответ требует назвать конкретные ловушки. Первая — дрейф: цикл «тик, потом работа, потом снова тик» накапливает задержку, потому что длительность работы задачи входит в интервал; лечится вычислением следующего момента вручную, и вот уже вы пишете свой мини-планировщик. Вторая — расписание в коде: «каждый будний день в 09:30» на тикере превращается в проверку дня недели и часа внутри горутины, а любое изменение — в правки кода и деплой. Третья — отсутствие служебной механики: у тикера нет понятия задачи, её идентификатора, снятия с расписания, часового пояса и защиты от паники. Ticker остаётся отличным инструментом для простой равномерной периодики — мы уже разбирали его вместе с time в уроке про таймеры и Ticker; проблема начинается, когда расписание становится человеческим. Как только появляется фраза «в 03:00 по будням», вы переизобретаете cron — дешевле взять готовый, подробное сравнение ждёт в уроке cron против Ticker.
Каждая задача robfig/cron запускается в собственной горутине, и паника внутри неё по умолчанию убивает всю программу — планировщик не ставит обёртку Recover автоматически. Одна забытая проверка на nil в ночном отчёте — и сервис лежит в три часа ночи, вместе со всеми остальными задачами. В v3 паники ловит обёртка cron.Recover из механизма wrappers: о ней отдельный урок про обёртки задач, и к нему стоит дойти до выкатки в продакшен.
Карта курса: от первого тика до боевого планировщика#
Курс идёт по нарастающей, и каждый урок опирается на предыдущие. Сначала фундамент: установка и первый планировщик, синтаксис пяти полей, спецсимволы и дескрипторы, задачи на функциях и задачи-структуры. Потом управление: часовые пояса, секундные расписания, Start и Stop, контекст отмены. Дальше — то, чего нет в туториалах и что отличает учебный проект от продакшена: перекрытие запусков долгих задач, конкурентность, обёртки Recover и SkipIfStillRunning, логирование и метрики, корректный graceful shutdown, утечки горутин и распределённые блокировки. Финал — итоговый проект: сервис отчётов, где всё это собрано вместе. Если вы уже пишете на Go и помните горутины из урока про конкурентность, вы готовы к курсу целиком: специальных знаний сверх базового синтаксиса здесь не потребуется.
Планировщик robfig/cron — сторонняя библиотека, которая работает внутри вашего процесса, поэтому код уроков выполняется локально: поставьте Go и соберите модуль командами из следующего урока. Песочница сайта умеет только подмножество стандартной библиотеки — fmt, strings, strconv, math, sort, errors и time — и сторонних пакетов в ней нет, так что крон-примеры в браузере не запускаются. Зато идеи из уроков про циклы и время можно потрогать прямо на странице: интерактивы курса работают всегда.
Проверка: где живёт планировщик
Где выполняется задача, зарегистрированная через AddFunc в robfig/cron?
Проверка: паника в задаче
Задача запаниковала в три часа ночи. Что произойдёт с сервисом по умолчанию?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
func main() {
c := cron.New()
_, _ = c.AddFunc("*/5 * * * *", func() {
fmt.Println("тик")
})
c.Start()
fmt.Println("стартовали")
}Соберите регистрацию задачи
Впишите три вызова по порядку жизни: создание планировщика, регистрация функции и запуск.
Соберите жизненный цикл планировщика
Расставьте строки по порядку выполнения: от создания планировщика до корректной остановки при завершении.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: знакомство с cron
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- cron в Go — это библиотека robfig/cron v3: планировщик живёт в вашем процессе, а не в системе.
- Каждая задача выполняется в собственной горутине; паника без обёртки Recover уронит весь сервис.
- Задача в процессе делит с сервисом конфиг, базу и логи; crontab запускает процесс целиком и живёт отдельно.
- time.Ticker хорош для равномерной периодики, но человеческое расписание «в 03:00 по будням» — территория cron.
- Регистрация задач — код инициализации: весь список расписаний виден в одном месте main.go.
- Start асинхронен: без вечной блокировки в main процесс завершится раньше первого тика.
- Песочница сайта не умеет сторонних библиотек: код курса запускается локально, нужен Go и go mod.
Домашнее задание
Создайте пустой модуль командой go mod init planner и добавьте robfig/cron/v3 через go get. Соберите программу из урока: планировщик с одной задачей «каждые пять минут» и печатью строки. Затем сломайте её трижды нарочно: уберите select {}, передайте кривое выражение «*/5 * *» без одного поля и вызовите Start дважды — запишите в заметки, что происходит в каждом случае и какую ошибку вы видите.
Личные заметкиПусто
Вопросы и ответы по уроку
robfig/cron — это тот же системный cron?
Нет. Системный cron — демон операционной системы, который запускает процессы целиком. robfig/cron — библиотека: планировщик работает внутри вашего Go-процесса, задача — функция в том же бинарнике, с тем же конфигом, базой и логами. Синтаксис выражений у них общий, отсюда и общее имя.
Что случится с задачами при рестарте сервиса?
Планировщик пересоздаётся заново: все задачи регистрируются при старте и продолжают по расписанию. Пропущенные тики не восполняются — если в момент простоя задача должна была выполниться, она просто не выполнится. Для задач, где пропуск критичен, ставят флаги «не выполнено с такого-то времени» в базе и догоняющий прогон.
Почему нельзя просто запускать задачи горутинами со Sleep?
На одной задаче — можно. На пяти уже появляются дрейф интервалов, человеческие расписания с днями недели и часами, пояса, снятие задач с расписания и защита от паники. Всё это robfig/cron даёт из коробки с проверенным парсером выражений — писать аналог самому дешевле только в учебных целях.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь