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

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

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

Урок 1 из 25 ~16 мин +20 XP

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

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

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

Типичная история знакомства с планировщиком выглядит так. Сервис-магазин отработал релиз, выкатил первые фичи, и тут бизнес просит «раз в сутки присылать владельцу отчёт по продажам». Раз в сутки — несложно: кто-то предлагает крон в системном 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 за пятнадцать строк

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; проблема начинается, когда расписание становится человеческим. Как только появляется фраза «в 03:00 по будням», вы переизобретаете cron — дешевле взять готовый, подробное сравнение ждёт в уроке cron против Ticker.

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

Курс идёт по нарастающей, и каждый урок опирается на предыдущие. Сначала фундамент: установка и первый планировщик, синтаксис пяти полей, спецсимволы и дескрипторы, задачи на функциях и задачи-структуры. Потом управление: часовые пояса, секундные расписания, Start и Stop, контекст отмены. Дальше — то, чего нет в туториалах и что отличает учебный проект от продакшена: перекрытие запусков долгих задач, конкурентность, обёртки Recover и SkipIfStillRunning, логирование и метрики, корректный graceful shutdown, утечки горутин и распределённые блокировки. Финал — итоговый проект: сервис отчётов, где всё это собрано вместе. Если вы уже пишете на Go и помните горутины из урока про конкурентность, вы готовы к курсу целиком: специальных знаний сверх базового синтаксиса здесь не потребуется.

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

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

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

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

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

Прочитайте код и выберите, что он выведет:

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

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

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

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

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

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

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

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

Строки

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

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

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

Термин

robfig/cron v3

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

Стандартный планировщик Go с синтаксисом классического cron. Живёт в вашем процессе, задачи — функции или структуры, запускаются по расписанию в отдельных горутинах.

1 / 6

Песочница урока

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

Личные заметкиПусто
Хранятся только в вашем браузере (Cron · Знакомство с cron: фоновые задачи в Go)

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

robfig/cron — это тот же системный cron?

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

Что случится с задачами при рестарте сервиса?

Планировщик пересоздаётся заново: все задачи регистрируются при старте и продолжают по расписанию. Пропущенные тики не восполняются — если в момент простоя задача должна была выполниться, она просто не выполнится. Для задач, где пропуск критичен, ставят флаги «не выполнено с такого-то времени» в базе и догоняющий прогон.

Почему нельзя просто запускать задачи горутинами со Sleep?

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

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

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

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

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

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

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

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