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

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

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

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

Синтаксис cron-выражений: пять полей

Синтаксис cron — это язык из пяти полей, на котором вы договариваетесь с планировщиком: минута, час, день месяца, месяц, день недели. Именно в этом формате robfig/cron ждёт выражение в AddFunc, и именно здесь чаще всего ошибаются: строка 0 9 * * 1-5 выглядит загадочно ровно до первого разобранного примера. Разберём все поля, их диапазоны, чтение вслух и главную ловушку с объединением дней.

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

Знакомая сцена: в код-ревью появляется строка из цифр и звёздочек, и половина команды делает вид, что всё понятно. Автор уверяет, что задача будет ходить по будням в девять, ревьюер сомневается, и спор заканчивается запуском в стейджинге — потому что вслух выражение никто так и не прочитал. Между тем пять полей — это фиксированный порядок, который не менялся с семидесятых: он одинаков у системного crontab, у Kubernetes CronJob и у robfig/cron. Умение прочитать строку вслух и перевести её в человеческое расписание — это навык на два часа практики, который потом экономит часы дежурств: неправильное выражение не падает, не ругается и просто тихо делает не то, что вы имели в виду. В этом уроке идём по порядку: поля слева направо, диапазоны каждого, чтение вслух на реальных примерах, и одна ловушка, из-за которой задачи запускаются в два раза чаще, чем планировал автор. К концу урока у вас появится шпаргалка проверки, которой можно делиться с командой на ревью.

Пять полей cron-выражения: слева направо#

Cron-выражение в robfig/cron v3 по умолчанию состоит ровно из пяти полей, разделённых пробелами. Слева направо: минуты от 0 до 59, часы от 0 до 23, день месяца от 1 до 31, месяц от 1 до 12 и день недели от 0 до 6, где ноль — воскресенье. Читается строка от меньшего к большему: сначала минута внутри часа, потом час внутри дня, и так до недели. Порядок фиксированный, его не поменять и не пропустить: пять значений обязаны присутствовать все, даже если поле означает «каждый раз» — на это место ставится звёздочка. Вместо чисел в полях месяца и дня недели разрешены имена: JAN-DEC и MON-SUN, регистр не важен, и сокращения из трёх букв читаются лучше, чем коды. Секунд в стандартном формате нет — пять полей закончились; секундное расписание включается отдельной опцией планировщика и разбирается в уроке про секунды и NewWithSeconds. Там же разобрана обидная ошибка миграции: шестипольное выражение со скопированными секундами не пройдёт парсер по умолчанию.

Поля выражения и их диапазоны#

ПолеДиапазонПример значенияЧто означает пример
Минуты0-5930тридцатая минута часа
Часы0-239-17с девяти утра до пяти вечера включительно
День месяца1-311первое число месяца
Месяц1-123-5с марта по май включительно
День недели0-6MON-FRIс понедельника по пятницу

Четыре выражения и что они значат

GO
// каждую субботу в полдень: дайджест недели
c.AddFunc("0 12 * * SAT", weeklyDigest)

// каждые полчаса в рабочие часы по будням
c.AddFunc("0,30 9-17 * * MON-FRI", officePing)

// первое число каждого месяца в 03:00: обслуживание
c.AddFunc("0 3 1 * *", monthlyMaintenance)

// каждый день ровно в полночь: чистка сессий
c.AddFunc("0 0 * * *", cleanupSessions)

Разберите каждую строку слева направо: минута, час, день месяца, месяц, день недели. Во второй строке список из двух минут работает вместе с диапазоном часов и дней недели — поля независимы, и задача пойдёт на пересечении условий. Четвёртая строка по смыслу совпадает с дескриптором @daily: полночь каждого дня — это и есть ноль минут, ноль часов, каждый день. Дескрипторы и звёздочки со слэшами — тема следующего урока.

Читаем выражение вслух: разбор строки из конфига#

Практический приём, который стоит закрепить в команде: любое новое выражение проговаривается словами. Возьмём строку из реального конфига: тридцать минут, восьмой час, каждое число, каждый месяц, будни. Складываем: в 08:30 с понедельника по пятницу. Обратите внимание, как единственное конкретное значение в поле минут и единственное в поле часов сузили время до одного момента в сутках, а звёздочка в днях месяца и месяцах позволила этому моменту повторяться ежедневно. Теперь наоборот: команда «каждое первое число квартала в полночь» собирается в строку из нулей и диапазона месяцев с шагом в три, и на этом шаге обычно и рождается ошибка — месяцы в cron нумеруются с единицы, кварталы задаются списком или диапазоном со слэшем, а не «номером квартала». Проговорённое вслух расписание ловит большинство опечаток ещё до запуска: человеческий язык не даёт перепутать день месяца с днём недели так легко, как строка из пяти полей.

Ловушка: день месяца и день недели объединяются через ИЛИ#

Самый дорогой сюрприз классического синтаксиса сидит в двух последних полях. Если и день месяца, и день недели ограничены — то есть в обоих стоят не звёздочки, — cron засчитывает совпадение любого из них, а не обоих сразу. Выражение с полднем, пятнадцатым числом и понедельником означает «15-го числа любого месяца ИЛИ каждый понедельник», и задача пойдёт в полдень по всем понедельникам, а не дважды в месяц, как хотел автор. Это поведение унаследовано robfig/cron v3 от классического парсера ради совместимости, и оно не меняется ни флагами, ни опциями. Выход простой и дисциплинарный: не ограничивайте оба поля одновременно. Одно из двух оставляйте звёздочкой, а второе условие переносите в код задачи — там день недели проверяется одной строкой. Либо используйте две отдельные задачи с двумя простыми выражениями: читаются они лучше, чем одно выражение с двойным смыслом, а заодно логируются и снимаются с расписания независимо друг от друга.

Проверка: правое поле

Какое поле стоит в выражении правее всех и что означает его значение 0?

Проверка: ИЛИ вместо И

Выражение 0 12 15 * 1 — когда выполнится задача?

Сколько раз за сутки?

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

_, err := c.AddFunc("0 */6 * * *", statusPing)
if err != nil {
        log.Fatal(err)
}
c.Start()

// вопрос: сколько раз за полные сутки
// выполнится statusPing?

Ошибка в выражении: парсер не молчит#

Хорошая новость синтаксиса v3: испортить расписание незаметно трудно. Строка проверяется парсером в момент регистрации — при вызове AddFunc — и проваливается немедленно, если поле вышло за диапазон: семьдесят пятая минута, тридцать второй час, тринадцатый месяц. Не проходит и число полей: шесть значений при пяти полях тоже отвергнуты — парсер вернёт ошибку про число полей, и AddFunc вернёт её вместе с нулевым идентификатором. Пустая строка и лишние пробелы ловятся тем же способом. Практика простая: выражения из конфига или базы проверяются на старте сервиса, до постановки задач — об этом следующий код.

Проверка выражений на старте через ParseStandard

GO
specs := map[string]string{
        "daily-report":    "0 3 * * *",
        "session-cleanup": "*/15 * * * *",
        "hourly-sync":     "0 * * * *",
}

for name, spec := range specs {
        if _, err := cron.ParseStandard(spec); err != nil {
                log.Fatalf("выражение %q из конфига неверно: %v", name, err)
        }
}

ParseStandard из пакета cron/v3 проверяет строку и возвращает расписание вместе с ошибкой — без регистрации задачи в планировщике. Здесь выражения пришли из конфига, и сервис отказывается стартовать, если хоть одно из них не разбирается: падение на деплое дешевле месяца тихо неисполняющейся задачи. Тот же вызов внутренне делает AddFunc, так что лишней проверки не будет — вопрос лишь в том, где вы хотите увидеть ошибку: на деплое со стеком окружения или в три часа ночи по логам.

Три шага проверки нового выражения#

  • Проговорите пять полей вслух и запишите человеческое расписание словами — расхождение с задуманным видно сразу.
  • Проверьте строку через cron.ParseStandard на старте сервиса: невалидное выражение должно валить деплой, а не молчать в проде.
  • После релиза посмотрите в лог на первые два запуска: время тика против ожидания — финальная проверка зоны и полей.

Соберите выражение для будней

Впишите значения полей: задача openShop должна ходить в 09:00 с понедельника по пятницу.

_, err := c.AddFunc("0 * * ", openShop) if err != nil { log.Fatal(err) }

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

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

Строки

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

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

Карточки: синтаксис пяти полей

Термин

Пять полей

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

Минуты (0-59), часы (0-23), день месяца (1-31), месяц (1-12), день недели (0-6). Порядок фиксированный, разделитель — пробел.

1 / 6

Теперь строка из пяти полей читается у вас вслух и не вызывает споров на ревью: поля фиксированы, диапазоны известны, ИЛИ между дневными полями держится в голове, а парсер ловит кривые значения на старте. Следующий шаг — обогатить выражения: в уроке про спецсимволы разберём звёздочку со слэшем, диапазоны, списки и дескрипторы @daily и @every, которые закрывают большинство «человеческих» расписаний без единой звезды. Если вы перескакиваете с шестипольных конфигов из старых статей — сначала загляните в урок про секунды, там объяснено, чем секундный режим отличается от стандартного. Жизненный цикл вокруг выражения — регистрация, идентификаторы, снятие задач — ждёт в уроке про Start и Stop. Полный маршрут курса — на странице курса; в домашнем задании — чтение и сборка выражений вручную.

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Cron-выражение v3 — пять полей: минуты, часы, день месяца, месяц, день недели; секунды только в отдельном режиме.
  • День недели считается с нуля: ноль — воскресенье; имена MON-SUN и JAN-DEC читаются надёжнее чисел.
  • Если ограничены и день месяца, и день недели, работает ИЛИ — задача пойдёт чаще, чем планировалось.
  • Парсер проверяет выражение при регистрации: диапазонные и структурные ошибки ловятся на старте.
  • ParseStandard проверяет строку из конфига без постановки задачи — падение на деплое дешевле молчания в проде.
  • Привычка проговаривать выражение вслух ловит большинство опечаток до ревью и до дежурства.

Домашнее задание

Выпишите из любого своего проекта три задачи, которые сегодня делаются вручную, и переведите их в cron-выражения: будничное утро, первое число месяца и шаг каждые N минут. Проговорите каждое вслух, соберите в map как в примере с ParseStandard и прогоните проверку в отдельной программе. Затем нарочно сломайте каждое выражение — выйдите за диапазон, уберите поле, добавьте шестое — и запишите, какую ошибку вернул парсер в каждом случае.

Личные заметкиПусто
Хранятся только в вашем браузере (Cron · Синтаксис cron-выражений: пять полей)

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

Почему день недели начинается с воскресенья?

Так сложилось исторически в классическом cron, и robfig/cron v3 сохраняет соглашение ради совместимости: 0 — воскресенье, 1 — понедельник, 6 — суббота. Если не хочется держать это в голове, используйте имена MON-SUN — они однозначны и читаются в коде и конфигах без шпаргалки.

Как задать «только рабочие дни»?

Ограничьте день недели диапазоном с понедельника по пятницу и не трогайте день месяца. Праздники классический синтаксис не знает: для них либо держите список исключений в конфиге и проверяйте его кодом задачи, либо ведите отдельное расписание. Совмещать ограничения в обоих дневных полях не стоит — заработает ИЛИ.

Обязательно ли проверять выражения из конфига?

Да, если конфиг правится людьми без деплоя кода. ParseStandard стоит ноль ресурсов и позволяет отказать в старте с понятной ошибкой вместо тишины в проде. AddFunc проверит то же самое, но проверка до регистрации даёт возможность отвергнуть весь конфиг целиком и показать оператору все ошибки сразу.

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

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

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

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

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

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

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