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

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

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

Урок 2 из 25 ~18 мин +20 XP

Установка robfig/cron и первый планировщик

Установка cron в golang-проект занимает одну команду, а первый рабочий планировщик — пятнадцать строк: go get github.com/robfig/cron/v3, cron.New, AddFunc и Start. В этом уроке соберём полноценную программу с проверкой ошибок, разберём сигнатуру AddFunc из v3, поймём, почему Start не блокирует main и как удержать процесс живым, чтобы первый тик всё-таки наступил.

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

Представим стандартную ситуацию: в репозитории уже лежит рабочий сервис, и задача — прикрутить к нему первую задачу по расписанию. Что бы ни было вокруг — 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.

Первый планировщик: полная программа

GO
package 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#

Порядок вызовов AddFunc и Start в robfig/cron свободный: можно зарегистрировать все задачи заранее и затем запустить, можно запустить планировщик и добавлять задачи по ходу работы — оба сценария поддержаны, и вторая половина курса пользуется этим активно, когда расписание приходит из базы или меняется через админку. Новая задача начинает жить по своему выражению сразу после регистрации: планировщик при следующем проходе вычислит для неё ближайший момент запуска. Внутри всё устроено честно: каждое выражение один раз разбирается парсером в структуру расписания, дальше остаётся только сверка времени, поэтому сотня задач на планировщике не создаёт никакой нагрузки. Ещё одна практическая деталь: AddFunc возвращает уникальный EntryID даже для одинаковых функций — одна и та же задача, добавленная дважды, выполнится дважды, и библиотека не станет вас предупреждать. Если подозреваете, что регистрация может выполниться повторно, проверяйте это в своём коде: планировщик доверчив, как словарь.

Регистрация задачи после запуска планировщика

GO
c := 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, импорт компилируется — дальше начинается синтаксис выражений.

Проверка: путь импорта

Какой путь импорта и имя пакета у третьей версии библиотеки?

Проверка: что вернёт 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("тик") }

Соберите инициализацию планировщика

Впишите путь пакета, конструктор, метод регистрации и запуск цикла.

import "github.com/robfig//v3" c := () _, err := c.("*/5 * * * *", ping) if err != nil { log.Fatal(err) } c.()

Соберите путь от пустой папки до работающего тика

Расставьте команды и строки программы в правильном порядке: сначала инициализация модуля, в конце — живой планировщик.

Строки

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

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

Карточки: установка и первый запуск

Термин

go get github.com/robfig/cron/v3

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

Команда установки: добавляет зависимость в go.mod. Импорт — github.com/robfig/cron/v3, имя пакета — cron. Мажорная версия обязана быть в пути импорта.

1 / 6

Фундамент под ногами: зависимость установлена, сигнатуры 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 отличается и какой вариант вам удобнее для локальной отладки.

Личные заметкиПусто
Хранятся только в вашем браузере (Cron · Установка robfig/cron и первый планировщик)

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

Нужен ли отдельный сервер или демон для robfig/cron?

Нет. Библиотека работает внутри вашего процесса: планировщик — горутина, задача — функция. Всё, что требуется, — установленная зависимость в go.mod. Отдельная инфраструктура появляется только при нескольких инстансах сервиса, когда задачи нужно не дублировать.

Чем v3 отличается от v2 при переходе?

Главное: AddFunc стал возвращать идентификатор EntryID вместе с ошибкой, появились секундные расписания через опцию WithSeconds, часовые пояса через WithLocation, цепочка обёрток WithChain и инспекция задач через Entries. Пути импорта различаются суффиксом, поэтому старый и новый код могут жить в разных модулях без конфликта.

Можно ли вызывать AddFunc после Start?

Да, планировщик продолжает принимать задачи на живом ходу: выражение разбирается, задача получает EntryID и ближайший момент запуска. Это основа динамического расписания — например, когда интервалы приходят из базы. Помните только, что библиотека не защищает от дублей: одна и та же задача, добавленная дважды, выполнится дважды.

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

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

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

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

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

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

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