---
title: "Установка robfig/cron и первый планировщик"
description: "Установка cron в golang-проект: go get robfig/cron v3, планировщик на cron.New, AddFunc и Start — программа с обработкой ошибок, EntryID и запуском в фоне."
course: "Фоновые задачи в Go: планировщик на robfig/cron"
url: "https://golangmaster.ru/courses/cron/lessons/02-ustanovka-robfig/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Фоновые задачи в Go: планировщик на robfig/cron», урок 2 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/cron/lessons/02-ustanovka-robfig/*

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

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

> **Main вышел — планировщик умер.** Start не блокирует управление: он поднимает горутину и возвращает управление в main. Если после Start в функции main больше нет работы, программа завершается — вместе со всеми задачами, до первого тика. Новички замечают это по симптомам: код скомпилировался, вывел «планировщик запущен» и упал в течение миллисекунды. Держите процесс живым осознанно: select {} в учебном коде, ожидание сигнала через os/signal в сервисе, а лучше — нормальный graceful shutdown, который разбирается в уроке про [остановку планировщика](/courses/cron/lessons/19-graceful-cron/).

### Задачи добавляются и до, и после 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](/courses/go-redis/lessons/14-pub-sub/): подписка появляется на живом соединении без переподключения. Динамическое расписание без рестарта — тема урока про [добавление задач на лету](/courses/cron/lessons/15-izmenenie-raspisaniya/), а проверки дублей делайте в своём коде: библиотека не отслеживает повторные регистрации.

### Внутри планировщика: таймер на ближайший запуск

Полезно один раз заглянуть под капот, чтобы дальше не удивляться поведению. После Start планировщик вычисляет для каждой зарегистрированной задачи ближайший момент запуска — метод Schedule.Next от текущего времени — и заводит таймер на самый ранний из этих моментов. Когда таймер срабатывает, все задачи с этим моментом уходят в собственные горутины, для каждой из них вычисляется следующий момент, и таймер заводится заново. Отсюда два практических вывода. Первый — про точность: пять полей означают минутную сетку, и задача выполнится в пределах своей минуты; секундная точность включается отдельной опцией и заслуживает отдельного урока, потому что там свои ловушки. Второй — про сдвиги времени: перевод часов обрабатывается через тот же вызов Next, но пересчёт происходит в момент срабатывания таймера, поэтому экзотические сценарии со скачками системных часов стоит проверять отдельно. Для типовых сервисов, которые тикают по минутам и часам, поведение ровно то, что ожидается: таймер сработал — задачи ушли — таймер заведён снова.

### Модуль, версии и где смотреть документацию

После go get в go.mod появляется строка с версией вида v3.0.1 — библиотека развивается неспешно и стабильно, мажорная тройка вышла в 2019 году и с тех пор сохраняет обратную совместимость сигнатур. Источник правды — pkg.go.dev/github.com/robfig/cron/v3: там лежат описания методов, опций и обёрток, и это стоит держать открытой вкладкой на первые пару недель. Структура модуля после установки не меняется: планировщик — это зависимость, а не отдельный слой проекта, и никаких папок вроде cron/ в своём дереве заводить не нужно. Единственное, что стоит решить заранее, — где будут жить регистрации задач: одна функция setupCron, принимающая зависимости и возвращающая готовый планировщик, спасёт main.go от превращения в свалку, когда задач станет больше пяти. Об этом приёме подробнее в уроке про [задачи-структуры](/courses/cron/lessons/06-addjob-structura/), а пока важно другое: установка закончена, зависимость в go.mod, импорт компилируется — дальше начинается синтаксис выражений.

> **Как проверить пример без сервера.** Планировщик — часть вашего процесса, поэтому код урока запускается локально: достаточно Go и двух команд go mod init и go get. Песочница сайта умеет только подмножество стандартной библиотеки — fmt, strings, strconv, math, sort, errors и time — и сторонних пакетов в ней нет, так что robfig/cron в браузере не заведётся. Компромисс для повторения темы на странице: интерактивы урока построены на тех же сигнатурах, что и код.

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

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

- gopkg.in/robfig/cron.v3, пакет cron
- ✅ github.com/robfig/cron/v3, пакет cron
- github.com/robfig/cron, пакет cron3
- github.com/cron/v3, пакет robfig

В Go мажорные версии начиная со второй обязаны присутствовать в конце пути модуля: github.com/robfig/cron/v3, имя пакета — cron. Путь gopkg.in/robfig/cron.v3 принадлежит старой линейке версий, и его сигнатуры не совпадают с v3.

### Проверка: что вернёт AddFunc

**Вопрос:** Что возвращает AddFunc в robfig/cron v3 при успешной регистрации?

- Только ошибку, как в первой версии
- Ничего — задача добавляется без обратной связи
- ✅ Идентификатор EntryID и ошибку
- Строку расписания и время следующего запуска

Сигнатура третьей версии: func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error). Идентификатор понадобится для снятия задач с расписания и их поиска, а ошибка возвращается при неудачном разборе выражения — её стоит проверять сразу на старте, а не в ночь первого тика. В первой версии библиотеки возвращалась только ошибка, и это первая вещь, по которой старый код не компилируется с v3.

### Что напечатает программа?

```go
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("тик") }
```

**Что выведет программа?**

- id = 1
- ✅ ошибка: true
- id = 0 и выполнение продолжится
- паника: неверное выражение

Поле часов в выражении может быть от 0 до 23, значение 25 парсер отвергает сразу при регистрации. AddFunc возвращает нулевой EntryID и ненулевую ошибку, код печатает «ошибка: true» и выходит. Именно поэтому проверку ошибки ставят на месте: молчаливое игнорирование превращается в задачу, которой не существует.

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

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

```go
import "github.com/robfig/___/v3"

c := ___()
_, err := c.___("*/5 * * * *", ping)
if err != nil {
        log.Fatal(err)
}
c.___()
```

Подсказка: Пакет называется cron, конструктор — cron.New, регистрация через AddFunc, запуск — Start.

Правильные ответы в пропусках: `cron`, `cron.New`, `AddFunc`, `Start`

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

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

Правильный порядок строк:

1. `go mod init planner`
2. `go get github.com/robfig/cron/v3`
3. `c := cron.New()`
4. `_, err := c.AddFunc("@hourly", syncData)`
5. `if err != nil { log.Fatal(err) }`
6. `c.Start()`

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

- **go get github.com/robfig/cron/v3** — Команда установки: добавляет зависимость в go.mod. Импорт — github.com/robfig/cron/v3, имя пакета — cron. Мажорная версия обязана быть в пути импорта.
- **cron.New()** — Конструктор планировщика с настройками по умолчанию: пять полей в выражениях, локальный часовой пояс. Возвращает указатель на Cron.
- **Сигнатура AddFunc** — func(spec string, cmd func()) (EntryID, error): выражение и функция без параметров; обратно — идентификатор задачи и ошибка парсинга. Проверка выражения происходит немедленно.
- **Start()** — Запускает цикл планировщика в отдельной горутине и сразу возвращается. Не блокирует main: без ожидания сигналов или select процесс завершится до первого тика.
- **EntryID** — Уникальный числовой идентификатор зарегистрированной задачи. Пригодится для поиска, снятия и замены задач: Entries, Entry, Remove.
- **signal.Notify** — Способ держать сервис живым и завершать его осознанно: процесс ждёт SIGINT/SIGTERM и по сигналу вызывает Stop — основа graceful shutdown планировщика.

Фундамент под ногами: зависимость установлена, сигнатуры v3 известны, полный цикл от go get до работающего тика помещается в одну программу. Дальше два урока посвящены тому, что стоит между кавычками AddFunc: в [третьем уроке](/courses/cron/lessons/03-sintaksis-cron/) разбирается синтаксис cron-выражений по полям — минуты, часы, дни, месяцы, дни недели, а в [четвёртом](/courses/cron/lessons/04-specsimvoly-cron/) — спецсимволы и дескрипторы вроде @daily. После них путь ответвляется: [задачи на функциях](/courses/cron/lessons/05-addfunc/) против [задач-структур](/courses/cron/lessons/06-addjob-structura/) — замыкание или метод с зависимостями, выбор станет понятен на реальных примерах. Ближе к продакшену вернёмся к жизненному циклу: [Start, Stop и идентификаторы задач](/courses/cron/lessons/09-start-stop/) — там EntryID из этого урока начнёт работать. Общий план — [на странице курса](/courses/cron/); домашнее задание ниже — про поломку примера, и это не издевательство: три намеренно сломанных запуска учат больше, чем один успешный.

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

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