Содержание урока · 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-59 | 30 | тридцатая минута часа |
| Часы | 0-23 | 9-17 | с девяти утра до пяти вечера включительно |
| День месяца | 1-31 | 1 | первое число месяца |
| Месяц | 1-12 | 3-5 | с марта по май включительно |
| День недели | 0-6 | MON-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
GOspecs := 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.NewWithSeconds и осознайте цену: расписание в секундах легко устроит дожим зависимости или API. Отдельный урок про секунды поднимет тему с примерами.
Планировщик robfig/cron живёт внутри вашего процесса, поэтому примеры курса выполняются локально: Go, go mod init и go get из второго урока. Песочница сайта умеет только подмножество стандартной библиотеки — fmt, strings, strconv, math, sort, errors и time — сторонних пакетов в ней нет. Зато интерактивы урока повторяют те же выражения, и их можно прокрутить прямо на странице.
Три шага проверки нового выражения#
- Проговорите пять полей вслух и запишите человеческое расписание словами — расхождение с задуманным видно сразу.
- Проверьте строку через cron.ParseStandard на старте сервиса: невалидное выражение должно валить деплой, а не молчать в проде.
- После релиза посмотрите в лог на первые два запуска: время тика против ожидания — финальная проверка зоны и полей.
Соберите выражение для будней
Впишите значения полей: задача openShop должна ходить в 09:00 с понедельника по пятницу.
Соберите регистрацию задачи на будни
Расставьте строки в порядке выполнения: создание планировщика, регистрация с проверкой, запуск и подтверждение в логе.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: синтаксис пяти полей
Теперь строка из пяти полей читается у вас вслух и не вызывает споров на ревью: поля фиксированы, диапазоны известны, ИЛИ между дневными полями держится в голове, а парсер ловит кривые значения на старте. Следующий шаг — обогатить выражения: в уроке про спецсимволы разберём звёздочку со слэшем, диапазоны, списки и дескрипторы @daily и @every, которые закрывают большинство «человеческих» расписаний без единой звезды. Если вы перескакиваете с шестипольных конфигов из старых статей — сначала загляните в урок про секунды, там объяснено, чем секундный режим отличается от стандартного. Жизненный цикл вокруг выражения — регистрация, идентификаторы, снятие задач — ждёт в уроке про Start и Stop. Полный маршрут курса — на странице курса; в домашнем задании — чтение и сборка выражений вручную.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Cron-выражение v3 — пять полей: минуты, часы, день месяца, месяц, день недели; секунды только в отдельном режиме.
- День недели считается с нуля: ноль — воскресенье; имена MON-SUN и JAN-DEC читаются надёжнее чисел.
- Если ограничены и день месяца, и день недели, работает ИЛИ — задача пойдёт чаще, чем планировалось.
- Парсер проверяет выражение при регистрации: диапазонные и структурные ошибки ловятся на старте.
- ParseStandard проверяет строку из конфига без постановки задачи — падение на деплое дешевле молчания в проде.
- Привычка проговаривать выражение вслух ловит большинство опечаток до ревью и до дежурства.
Домашнее задание
Выпишите из любого своего проекта три задачи, которые сегодня делаются вручную, и переведите их в cron-выражения: будничное утро, первое число месяца и шаг каждые N минут. Проговорите каждое вслух, соберите в map как в примере с ParseStandard и прогоните проверку в отдельной программе. Затем нарочно сломайте каждое выражение — выйдите за диапазон, уберите поле, добавьте шестое — и запишите, какую ошибку вернул парсер в каждом случае.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему день недели начинается с воскресенья?
Так сложилось исторически в классическом cron, и robfig/cron v3 сохраняет соглашение ради совместимости: 0 — воскресенье, 1 — понедельник, 6 — суббота. Если не хочется держать это в голове, используйте имена MON-SUN — они однозначны и читаются в коде и конфигах без шпаргалки.
Как задать «только рабочие дни»?
Ограничьте день недели диапазоном с понедельника по пятницу и не трогайте день месяца. Праздники классический синтаксис не знает: для них либо держите список исключений в конфиге и проверяйте его кодом задачи, либо ведите отдельное расписание. Совмещать ограничения в обоих дневных полях не стоит — заработает ИЛИ.
Обязательно ли проверять выражения из конфига?
Да, если конфиг правится людьми без деплоя кода. ParseStandard стоит ноль ресурсов и позволяет отказать в старте с понятной ошибкой вместо тишины в проде. AddFunc проверит то же самое, но проверка до регистрации даёт возможность отвергнуть весь конфиг целиком и показать оператору все ошибки сразу.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь