Представим стандартную ситуацию: в репозитории уже лежит рабочий сервис, и задача — прикрутить к нему первую задачу по расписанию. Что бы ни было вокруг — HTTP-сервер, бот, обработчик очереди — механика одна и та же: добавить зависимость в модуль, создать планировщик, зарегистрировать функцию, запустить цикл. robfig cron устроен скромно и предсказуемо: у него нет конфигурационных файлов, YAML-схем и магии со сторонами света, весь публичный интерфейс помещается на один экран. Именно поэтому первый планировщик удобно собрать до конца, со всеми проверками ошибок, а не в виде фрагмента из туториала: ошибки парсинга выражений в v3 ловятся в момент регистрации, и это стоит увидеть в первый же день. Двигаемся по шагам: модуль, зависимость, импорт, программа целиком, разбор каждой строки и две ловушки, на которых ломают первые запуски.
Установка robfig cron: go get и путь импорта#
robfig cron ставится как любая Go-зависимость. В корне модуля выполняется go get github.com/robfig/cron/v3 — команда допишет версию в go.mod и скачает пакет; дальше в коде импортируется путь github.com/robfig/cron/v3, а сам пакет называется просто cron. Обратите внимание на суффикс /v3: в Go мажорная версия второй и старше обязана жить в конце пути импорта, поэтому перепутать версии сложно — v3 не может подменить собой v2. Это же суёт под нос главное отличие тройки: старые статьи про gopkg.in/robfig/cron.v1 и github.com/robfig/cron/v2 описывают другой API, где AddFunc возвращал только ошибку, а часовых поясов и обёрток ещё не было. Если код из чужого блога не компилируется с «cannot use ... as EntryID» — почти всегда виновата смешанная версия. Установка cron для golang-проекта этим и ограничивается: никакой отдельной службы, демона или драйвера не нужно, зависимость чисто компиляционная и работает на любой платформе, где собирается Go.
Первый планировщик: полная программа
GOpackage main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/robfig/cron/v3"
)
func main() {
c := cron.New()
_, err := c.AddFunc("*/5 * * * *", func() {
fmt.Println("проверка сервера:", time.Now().Format(time.TimeOnly))
})
if err != nil {
log.Fatalf("выражение не разобрано: %v", err)
}
c.Start()
fmt.Println("планировщик запущен, ждём сигналов")
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
c.Stop()
fmt.Println("планировщик остановлен")
}Перед импортом выполните в корне модуля две команды: go mod init planner и go get github.com/robfig/cron/v3 — без них компилятор не найдёт пакет. В программе четыре смысловых шага: конструктор cron.New, регистрация задачи через AddFunc с проверкой ошибки, Start и ожидание сигнала завершения. Ожидание через signal.Notify — продакшен-версия select {}: процесс живёт, пока его не остановит оператор, и успевает вызвать Stop.
cron.New, AddFunc и Start: разбор сигнатур v3#
cron.New() возвращает указатель на планировщик с настройками по умолчанию: пять полей в выражениях, локальный часовой пояс, без обёрток задач. Метод AddFunc принимает два аргумента — строку выражения и функцию без параметров и без возвращаемого значения — и возвращает пару значений: EntryID и error. Сигнатура func(spec string, cmd func()) (EntryID, error) — маркер третьей версии: парсер проверяет выражение немедленно, и опечатка вроде «0 25 *» вернёт ошибку прямо при старте, а не в три часа ночи молчанием. EntryID — целочисленный идентификатор зарегистрированной задачи; на этом уроке он просто печатается, но дальше станет инструментом: по нему снимают задачи с расписания и находят их среди прочих. Start() запускает внутренний цикл в отдельной горутине и возвращается мгновенно — это второй маркер v3, знакомый по документации: планировщик не управляет временем жизни main, и об этом — следующий блок.
Start не блокирует управление: он поднимает горутину и возвращает управление в main. Если после Start в функции main больше нет работы, программа завершается — вместе со всеми задачами, до первого тика. Новички замечают это по симптомам: код скомпилировался, вывел «планировщик запущен» и упал в течение миллисекунды. Держите процесс живым осознанно: select {} в учебном коде, ожидание сигнала через os/signal в сервисе, а лучше — нормальный graceful shutdown, который разбирается в уроке про остановку планировщика.
Задачи добавляются и до, и после Start#
Порядок вызовов AddFunc и Start в robfig/cron свободный: можно зарегистрировать все задачи заранее и затем запустить, можно запустить планировщик и добавлять задачи по ходу работы — оба сценария поддержаны, и вторая половина курса пользуется этим активно, когда расписание приходит из базы или меняется через админку. Новая задача начинает жить по своему выражению сразу после регистрации: планировщик при следующем проходе вычислит для неё ближайший момент запуска. Внутри всё устроено честно: каждое выражение один раз разбирается парсером в структуру расписания, дальше остаётся только сверка времени, поэтому сотня задач на планировщике не создаёт никакой нагрузки. Ещё одна практическая деталь: AddFunc возвращает уникальный EntryID даже для одинаковых функций — одна и та же задача, добавленная дважды, выполнится дважды, и библиотека не станет вас предупреждать. Если подозреваете, что регистрация может выполниться повторно, проверяйте это в своём коде: планировщик доверчив, как словарь.
Регистрация задачи после запуска планировщика
GOc := cron.New()
c.Start()
fmt.Println("планировщик уже работает")
// спустя время — например, по запросу из админки
id, err := c.AddFunc("30 9 * * MON-FRI", sendMorningDigest)
if err != nil {
log.Printf("задача не добавлена: %v", err)
return
}
fmt.Println("новая задача:", id)Планировщик уже запущен, но AddFunc работает как обычно: выражение разбирается, задача получает идентификатор и ближайший момент запуска. Похоже на тот же паттерн, что в подписках — в go-redis мы видели его в уроке про Pub/Sub: подписка появляется на живом соединении без переподключения. Динамическое расписание без рестарта — тема урока про добавление задач на лету, а проверки дублей делайте в своём коде: библиотека не отслеживает повторные регистрации.
Внутри планировщика: таймер на ближайший запуск#
Полезно один раз заглянуть под капот, чтобы дальше не удивляться поведению. После Start планировщик вычисляет для каждой зарегистрированной задачи ближайший момент запуска — метод Schedule.Next от текущего времени — и заводит таймер на самый ранний из этих моментов. Когда таймер срабатывает, все задачи с этим моментом уходят в собственные горутины, для каждой из них вычисляется следующий момент, и таймер заводится заново. Отсюда два практических вывода. Первый — про точность: пять полей означают минутную сетку, и задача выполнится в пределах своей минуты; секундная точность включается отдельной опцией и заслуживает отдельного урока, потому что там свои ловушки. Второй — про сдвиги времени: перевод часов обрабатывается через тот же вызов Next, но пересчёт происходит в момент срабатывания таймера, поэтому экзотические сценарии со скачками системных часов стоит проверять отдельно. Для типовых сервисов, которые тикают по минутам и часам, поведение ровно то, что ожидается: таймер сработал — задачи ушли — таймер заведён снова.
Модуль, версии и где смотреть документацию#
После go get в go.mod появляется строка с версией вида v3.0.1 — библиотека развивается неспешно и стабильно, мажорная тройка вышла в 2019 году и с тех пор сохраняет обратную совместимость сигнатур. Источник правды — pkg.go.dev/github.com/robfig/cron/v3: там лежат описания методов, опций и обёрток, и это стоит держать открытой вкладкой на первые пару недель. Структура модуля после установки не меняется: планировщик — это зависимость, а не отдельный слой проекта, и никаких папок вроде cron/ в своём дереве заводить не нужно. Единственное, что стоит решить заранее, — где будут жить регистрации задач: одна функция setupCron, принимающая зависимости и возвращающая готовый планировщик, спасёт main.go от превращения в свалку, когда задач станет больше пяти. Об этом приёме подробнее в уроке про задачи-структуры, а пока важно другое: установка закончена, зависимость в go.mod, импорт компилируется — дальше начинается синтаксис выражений.
Планировщик — часть вашего процесса, поэтому код урока запускается локально: достаточно Go и двух команд go mod init и go get. Песочница сайта умеет только подмножество стандартной библиотеки — fmt, strings, strconv, math, sort, errors и time — и сторонних пакетов в ней нет, так что robfig/cron в браузере не заведётся. Компромисс для повторения темы на странице: интерактивы урока построены на тех же сигнатурах, что и код.
Проверка: путь импорта
Какой путь импорта и имя пакета у третьей версии библиотеки?
Проверка: что вернёт AddFunc
Что возвращает AddFunc в robfig/cron v3 при успешной регистрации?
Что напечатает программа?
Прочитайте код и выберите, что он выведет:
func main() {
c := cron.New()
id, err := c.AddFunc("0 25 * * *", tick)
if err != nil {
fmt.Println("ошибка:", err != nil)
return
}
fmt.Println("id =", id)
}
func tick() { fmt.Println("тик") }Соберите инициализацию планировщика
Впишите путь пакета, конструктор, метод регистрации и запуск цикла.
Соберите путь от пустой папки до работающего тика
Расставьте команды и строки программы в правильном порядке: сначала инициализация модуля, в конце — живой планировщик.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: установка и первый запуск
Фундамент под ногами: зависимость установлена, сигнатуры v3 известны, полный цикл от go get до работающего тика помещается в одну программу. Дальше два урока посвящены тому, что стоит между кавычками AddFunc: в третьем уроке разбирается синтаксис cron-выражений по полям — минуты, часы, дни, месяцы, дни недели, а в четвёртом — спецсимволы и дескрипторы вроде @daily. После них путь ответвляется: задачи на функциях против задач-структур — замыкание или метод с зависимостями, выбор станет понятен на реальных примерах. Ближе к продакшену вернёмся к жизненному циклу: Start, Stop и идентификаторы задач — там EntryID из этого урока начнёт работать. Общий план — на странице курса; домашнее задание ниже — про поломку примера, и это не издевательство: три намеренно сломанных запуска учат больше, чем один успешный.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Установка — одна команда: go get github.com/robfig/cron/v3; пакет импортируется как cron, мажорная версия живёт в пути.
- cron.New() даёт планировщик по умолчанию: пять полей, локальная зона, без обёрток.
- AddFunc в v3 возвращает (EntryID, error): выражение проверяется при регистрации, ошибки не ждут ночного тика.
- Start асинхронен: main держат живым через select, сигнал os/signal или graceful shutdown.
- Задачи можно добавлять и до, и после Start — это база динамического расписания.
- Дублирование AddFunc не преследуется библиотекой: одна задача, добавленная дважды, выполнится дважды.
- Код запускается локально: песочница умеет только fmt, strings, strconv, math, sort, errors и time.
Домашнее задание
Соберите модуль planner с планировщиком из урока и добавьте в него три задачи: раз в минуту печатать текущее время, раз в пять минут — фиксированную строку, по @hourly — сумму двух констант. Затем добавьте четвёртую регистрацию с заведомо неверным выражением, где поле часов равно 25, и убедитесь, что программа падает на log.Fatalf до первого тика. В конце замените ожидание сигнала на select {} и проверьте, что Ctrl+C больше не даёт программе вызвать Stop. Запишите в заметки, чем поведение двух версий main отличается и какой вариант вам удобнее для локальной отладки.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужен ли отдельный сервер или демон для robfig/cron?
Нет. Библиотека работает внутри вашего процесса: планировщик — горутина, задача — функция. Всё, что требуется, — установленная зависимость в go.mod. Отдельная инфраструктура появляется только при нескольких инстансах сервиса, когда задачи нужно не дублировать.
Чем v3 отличается от v2 при переходе?
Главное: AddFunc стал возвращать идентификатор EntryID вместе с ошибкой, появились секундные расписания через опцию WithSeconds, часовые пояса через WithLocation, цепочка обёрток WithChain и инспекция задач через Entries. Пути импорта различаются суффиксом, поэтому старый и новый код могут жить в разных модулях без конфликта.
Можно ли вызывать AddFunc после Start?
Да, планировщик продолжает принимать задачи на живом ходу: выражение разбирается, задача получает EntryID и ближайший момент запуска. Это основа динамического расписания — например, когда интервалы приходят из базы. Помните только, что библиотека не защищает от дублей: одна и та же задача, добавленная дважды, выполнится дважды.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь