---
title: "Знакомство с cron: фоновые задачи в Go"
description: "Что такое golang cron: фоновые задачи go-сервиса на robfig/cron v3, планировщик против системного crontab, ловушки time.Ticker и карта курса из 25 уроков."
course: "Фоновые задачи в Go: планировщик на robfig/cron"
url: "https://golangmaster.ru/courses/cron/lessons/01-znakomstvo-cron-go/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Знакомство с cron: фоновые задачи в Go

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

Фоновые задачи в Go появляются в каждом сервисе, который живёт дольше месяца: ночной отчёт, чистка просроченных сессий, напоминания пользователям. golang cron в лице библиотеки robfig/cron v3 — де-факто стандартный способ держать такую работу по расписанию внутри процесса. В этом вводном уроке разберём, где живёт планировщик, чем он лучше системного crontab и почему наивный time.Sleep не выдерживает продакшена.

Типичная история знакомства с планировщиком выглядит так. Сервис-магазин отработал релиз, выкатил первые фичи, и тут бизнес просит «раз в сутки присылать владельцу отчёт по продажам». Раз в сутки — несложно: кто-то предлагает крон в системном crontab, кто-то горутину с time.Sleep в цикле. Через месяц задач уже пять: напомнить неоплатившим корзину, удалить брошенные черновики, синхронизировать остатки с внешним API, перечитать курсы валют, отправить дайджест. У каждой задачи своё время, свои часовые пояса, свои требования к ошибкам — и внезапно оказывается, что пять sleep-горутин в main() больше не поддаются чтению. В экосистеме Go эту боль решает robfig/cron: планировщик с синтаксисом классического cron, который живёт внутри вашего приложения, а не где-то в системе. Весь курс построен на третьей версии библиотеки — v3, потому что именно она умеет часовые пояса, секундные расписания и обёртки задач. Полный маршрут из двадцати пяти уроков лежит на [странице курса](/courses/cron/); здесь — карта местности, чтобы дальше не блуждать.

### Что такое cron в Go: планировщик внутри вашего процесса

Cron в Go — это не отдельный демон и не настройка операционной системы, а обычная библиотека, которую вы импортируете и запускаете в своём процессе. robfig/cron создаёт внутри программы цикл, который раз в минуту просыпается, сверяет текущее время с зарегистрированными расписаниями и запускает те задачи, что подошли. Каждая запущенная задача получает собственную горутину, поэтому одна длинная задача не мешает другим тикать. Отсюда важное следствие: планировщик живёт ровно столько, сколько живёт процесс. Перезапустили приложение — расписание поднялось заново, задачи пошли по новым часам. Упал процесс — задачи замолчали вместе с ним, и это честно: фоновые задачи не обязаны переживать смерть сервиса, который их породил. Такое решение быстрее всего объяснить через то, чего оно даёт: задача по расписанию разделяет с сервисом конфиги, подключение к базе, логгер и правила graceful shutdown, а не договаривается с ним через файлы и переменные окружения.

### Фоновые задачи go-сервиса: типовой список

- Ночные отчёты и дайджесты: раз в сутки собрать агрегаты и отправить письмом или в чат.
- Чистка: удалить просроченные сессии, временные файлы, неотправленные уведомления старше недели.
- Напоминания: пингануть пользователей о неоплаченной корзине через час после брошенного шага.
- Синхронизация: перечитать курсы валют, слить изменения с внешним API каждые N минут.
- Обслуживание: бэкап, ротация логов, пересчёт кэша в часы минимальной нагрузки.
- Мониторинг: раз в пять минут проверять внешние сервисы и слать алерт при деградации.

### Системный crontab против планировщика в приложении

Классический ответ на задачу «запускать по расписанию» — системный crontab: демон cron операционной системы читает файл с расписаниями и запускает указанные процессы целиком. Схема рабочая, но у неё есть цена, которую видно изнутри Go-сервиса. Cron запускает новый процесс: ему нужны бинарник, переменные окружения, доступ к базе — всё это дублируется отдельно от основного приложения, и конфиги начинают жить в двух местах. Процесс не знает о состоянии сервиса: задача может стартовать, пока сервис в деплое, и работать против старой базы. Логи и ошибки задач живут в syslog, отдельно от остального. И главное — расписание статично: поменять его значит отредактировать файл на сервере. Планировщик в приложении переворачивает картину: задача — это функция в том же бинарнике, с тем же конфигом и тем же подключением к базе; расписание читается из кода или конфига и меняется обычным деплоем; ошибки идут в общий логгер. Единственное, за что приходится платить, — задачи умирают вместе с процессом и дублируются, если сервис масштабируется в несколько инстансов: про это отдельный урок про [блокировки при нескольких инстансах](/courses/cron/lessons/21-raspredelennye-blokirovki/).

### Три способа запускать периодические задачи

| Способ | Где живёт | Сильная сторона | Типовая проблема |
| --- | --- | --- | --- |
| Системный crontab | демон ОС, вне приложения | переживает рестарт сервиса, ничего не стоит | дублирование конфигов, задачи оторваны от кода |
| robfig/cron v3 | горутина внутри процесса | общий конфиг, БД, логи; расписание меняется деплоем | умирает с процессом, дубли при нескольких инстансах |
| time.Ticker в цикле | своя горутина, своё решение | нет зависимости, полный контроль | дрейф времени, нет парсинга расписаний и управления задачами |

### Первый взгляд: robfig/cron за пятнадцать строк

```go
package 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](/courses/go-advanced/lessons/23-time/); проблема начинается, когда расписание становится человеческим. Как только появляется фраза «в 03:00 по будням», вы переизобретаете cron — дешевле взять готовый, подробное сравнение ждёт в уроке [cron против Ticker](/courses/cron/lessons/24-ticker-vs-cron/).

> **Паника в задаче уронит весь процесс.** Каждая задача robfig/cron запускается в собственной горутине, и паника внутри неё по умолчанию убивает всю программу — планировщик не ставит обёртку Recover автоматически. Одна забытая проверка на nil в ночном отчёте — и сервис лежит в три часа ночи, вместе со всеми остальными задачами. В v3 паники ловит обёртка cron.Recover из механизма wrappers: о ней отдельный урок про [обёртки задач](/courses/cron/lessons/16-wrappers/), и к нему стоит дойти до выкатки в продакшен.

### Карта курса: от первого тика до боевого планировщика

Курс идёт по нарастающей, и каждый урок опирается на предыдущие. Сначала фундамент: [установка и первый планировщик](/courses/cron/lessons/02-ustanovka-robfig/), [синтаксис пяти полей](/courses/cron/lessons/03-sintaksis-cron/), [спецсимволы и дескрипторы](/courses/cron/lessons/04-specsimvoly-cron/), [задачи на функциях](/courses/cron/lessons/05-addfunc/) и [задачи-структуры](/courses/cron/lessons/06-addjob-structura/). Потом управление: часовые пояса, секундные расписания, Start и Stop, контекст отмены. Дальше — то, чего нет в туториалах и что отличает учебный проект от продакшена: перекрытие запусков долгих задач, конкурентность, обёртки Recover и SkipIfStillRunning, логирование и метрики, корректный graceful shutdown, утечки горутин и распределённые блокировки. Финал — итоговый проект: сервис отчётов, где всё это собрано вместе. Если вы уже пишете на Go и помните горутины из [урока про конкурентность](/courses/go-advanced/lessons/04-gorutiny/), вы готовы к курсу целиком: специальных знаний сверх базового синтаксиса здесь не потребуется.

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

### Проверка: где живёт планировщик

**Вопрос:** Где выполняется задача, зарегистрированная через AddFunc в robfig/cron?

- В отдельном процессе, который создаёт библиотека
- ✅ В горутине внутри вашего приложения
- В системном демоне cron, если он установлен
- В отдельном потоке операционной системы вне Go

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

### Проверка: паника в задаче

**Вопрос:** Задача запаниковала в три часа ночи. Что произойдёт с сервисом по умолчанию?

- Планировщик перезапустит только эту задачу
- Задача молча завершится, сервис продолжит работать
- ✅ Упадёт весь процесс: паника в горутине не ловится автоматически
- Паника записывается в лог, и планировщик останавливается

Каждый тик — своя горутина, а непойманная паника в любой горутине убивает программу целиком. robfig/cron v3 не оборачивает задачи Recover по умолчанию: защита ставится явно через cron.WithChain и cron.Recover, о них будет отдельный урок про обёртки.

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

```go
func main() {
    c := cron.New()

    _, _ = c.AddFunc("*/5 * * * *", func() {
        fmt.Println("тик")
    })

    c.Start()
    fmt.Println("стартовали")
}
```

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

- стартовали, затем тик через пять минут
- тик, затем стартовали
- ✅ только «стартовали»: main завершится раньше первого тика
- ошибка компиляции: Start должен вызываться раньше AddFunc

Start запускает планировщик асинхронно и сразу возвращает управление, но main после печати строки завершается — вместе с процессом и планировщиком. Первый тик по выражению «каждые пять минут» настал бы через пять минут, которых у программы нет. Держать main живым нужно явно: select, чтение сигналов или другая работа процесса.

### Соберите регистрацию задачи

Впишите три вызова по порядку жизни: создание планировщика, регистрация функции и запуск.

```go
c := cron.___()

_, err := c.___("*/5 * * * *", func() {
        fmt.Println("проверка сервера")
})
if err != nil {
        log.Fatal(err)
}

c.___()
```

Подсказка: Планировщик создаёт конструктор New, функция регистрируется через AddFunc, запускает цикл метод Start.

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

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

Расставьте строки по порядку выполнения: от создания планировщика до корректной остановки при завершении.

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

1. `c := cron.New()`
2. `id, err := c.AddFunc("@daily", cleanupSessions)`
3. `if err != nil { log.Fatal(err) }`
4. `c.Start()`
5. `fmt.Println("задача зарегистрирована:", id)`
6. `c.Stop()`

### Карточки: знакомство с cron

- **robfig/cron v3** — Стандартный планировщик Go с синтаксисом классического cron. Живёт в вашем процессе, задачи — функции или структуры, запускаются по расписанию в отдельных горутинах.
- **Планировщик в процессе** — Задачи делят с сервисом конфиги, БД, логгер и graceful shutdown. Расписание меняется деплоем. Обратная сторона: умирает с процессом и дублируется при нескольких инстансах.
- **Системный crontab** — Демон ОС запускает процессы целиком по расписанию. Не знает о приложении: отдельные конфиги, логи в syslog, статичное расписание в файле на сервере.
- **AddFunc** — Метод планировщика: регистрирует функцию с cron-выражением. Сигнатура v3 возвращает два значения: идентификатор задачи EntryID и ошибку парсинга выражения.
- **Паника в задаче** — По умолчанию убивает весь процесс: горутина без recover падает вместе с программой. Лечится обёрткой cron.Recover через опцию WithChain.
- **select {} в main** — Вечная блокировка, которая держит процесс живым. Без неё main завершается сразу после Start: планировщик асинхронен и не ждёт первого тика.

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

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