Выражение из одних звёздочек — самая неточная команда, какую можно дать планировщику: «каждую минуту, всегда». Реальные задачи почти всегда требуют сужения: не каждую минуту, а каждые десять; не каждый день, а по будням; не в произвольный момент, а в полдень. Для этого в синтаксисе есть четыре спецсимвола и один особый случай — дескрипторы с собакой, которые robfig/cron поддерживает поверх классики. Символы не заучиваются: их ровно четыре, каждый читается одним словом, а путаница возникает в двух местах — к чему привязан шаг и чем @every отличается от выражения с полем часов. Эти два вопроса мы разберём отдельно и с примерами, потому что именно на них строятся неверные ожидания: задача запускается в правильные, но не те моменты, и код при этом безупречен. Хорошая новость в том, что словарь конечен: освоив восемь конструкций этого урока, вы сможете записать практически любое расписание, которое встретите в требованиях.
Звёздочка и слэш: шаг по расписанию#
Звёздочка в поле означает весь диапазон значений этого поля: каждая минута, каждый час, каждое число. Сама по себе она редко встречается во всех пяти полях — чаще её комбинируют, оставляя звёздочкой то, что не ограничиваем. Слэш задаёт шаг: выражение */5 в поле минут — это каждая пятая минута по сетке часа, то есть 00, 05, 10 и так до 55. Ключевая деталь, которую многие пропускают: шаг отсчитывается от начала поля, а не от момента запуска планировщика. Процесс мог подняться в 13:42, но задача с шагом в десять минут всё равно пойдёт в 13:50, а не через десять минут после старта. Сетка стабильна и одинакова у всех экземпляров сервиса — это удобно для задач, где важна синхронность: отчёты собираются по одним и тем же границам, какие бы рестарты ни случались. Минимальный шаг ограничен полем: в минутах меньше единицы не бывает, для более частой работы нужен секундный режим из отдельного урока.
Шпаргалка спецсимволов#
| Символ | Читается как | Пример в поле минут | Что означает пример |
|---|---|---|---|
| звёздочка | каждое значение | звёздочка | каждая минута часа |
| слэш | шаг | */5 | 00, 05, 10 — каждую пятую минуту |
| дефис | диапазон | 10-20 | с 10-й по 20-ю минуту включительно |
| запятая | список | 0,15,30,45 | четыре момента каждый час |
| собака | дескриптор | @hourly | в начале каждого часа |
Диапазоны, списки и имена полей#
Дефис превращает два числа в диапазон: 9-17 в поле часов — это с девяти утра до пяти вечера включительно. Запятая перечисляет значения: 0,15,30,45 — четыре момента каждый час. Оба приёма работают в любом поле и свободно комбинируются с шагом и звёздочкой, а в полях месяца и дня недели вместо чисел разрешены имена: JAN-DEC и MON-SUN — регистр не важен. Имена — не украшение: строка с MON-FRI читается без шпаргалки, а выражение с числом 1 в последнем поле постоянно провоцирует вопрос «понедельник или воскресенье?». Внутри диапазона и списка можно мешать имена с числами — парсер приведёт их к одному виду. Единственное правило вкуса: не собирайте в одном выражении всё сразу. Когда в строке одновременно живут шаг, диапазон, список и имена, она перестаёт читаться на ревью, и проще разбить задачу на две с двумя простыми выражениями — планировать их можно по-прежнему в одном планировщике.
Комбинируем спецсимволы в AddFunc
GO// каждые 10 минут в рабочие часы по будням
c.AddFunc("*/10 9-17 * * MON-FRI", healthProbe)
// полдень и шесть вечера по выходным
c.AddFunc("0 12,18 * * SAT,SUN", weekendDigest)
// с 10-й по 45-ю минуту шагом в пять
c.AddFunc("10-45/5 * * * *", reportChunk)
// интервал без привязки к часам
c.AddFunc("@every 90m", syncRates)Первая строка — типовой продакшен-профилинг: шаг поверх диапазона часов, будни в поле дней. Вторая показывает список в поле часов и список дней: поля независимы, задача идёт на пересечении. Третья — шаг, применённый к диапазону минут: 10, 15, 20 и так до 45, после чего тишина до следующего часа. Четвёртая — интервальный дескриптор, о нём подробнее ниже.
Шаг поверх диапазона и особый случай N-со-слэшем#
Шаг можно вешать не только на звёздочку, но и на диапазон: 10-45/5 в поле минут означает 10, 15, 20 и так до 45 — включительно, потому что цикл парсера идёт до конца диапазона. Это удобно для окон: полчаса интенсивной проверки внутри часа, потом тишина. Отдельного упоминания заслуживает форма из одного числа и слэша. В robfig/cron v3 она документирована прямо в исходнике парсера: N/шаг трактуется как N-максимум/шаг, то есть 3/10 в минутах — это 3, 13, 23 и далее до 53. Не все планировщики трактуют эту форму одинаково, поэтому в переносимых конфигах надёжнее писать диапазон целиком: 3-59/10 говорит без двусмысленностей и читается одинаково у всех. Правило то же, что с ИЛИ в дневных полях: неоднозначные формы лучше не использовать не потому, что они сломаны, а потому что их смысл варьируется от системы к системе, а расписания живут дольше, чем команда, которая их писала.
Дескрипторы: @daily, @hourly и @every#
Поверх пяти полей robfig/cron v3 поддерживает дескрипторы — читаемые сокращения для типовых расписаний. @yearly и его синоним @annually — раз в год, первого января в полночь; @monthly — первое число каждого месяца; @weekly — воскресенье в полночь; @daily и @midnight — каждый день в 00:00; @hourly — в начале каждого часа. Дескриптор разворачивается в обычное выражение в зоне планировщика, поэтому @daily в сервисе с московской зоной идёт в полночь по Москве — про пояса подробно в уроке про часовые пояса. Отдельно стоит @every — единственный дескриптор, который не привязан к часам вообще: после него пишется длительность в формате time.Duration, например @every 1h30m или @every 90m, и задача ходит с этим интервалом. Минимальный шаг — одна секунда, меньшие значения парсер округлит вверх. Дескрипторы не добавляют новых возможностей — то же самое пишется пятью полями, — но выражение «@daily» читается быстрее, чем пять нулей и звёздочек, а в конфигах читаемость напрямую снижает число ошибок. Внутри библиотеки каждый дескриптор собирает ту же битовую маску значений поля, что и разобранная строка: разницы в поведении и производительности нет.
Шесть дескрипторов одним списком
GOc.AddFunc("@yearly", archiveYear) // 1 января, 00:00
c.AddFunc("@annually", archiveYear) // синоним @yearly
c.AddFunc("@monthly", billCustomers) // 1-го числа, 00:00
c.AddFunc("@weekly", weeklyDigest) // воскресенье, 00:00
c.AddFunc("@daily", cleanupSessions) // каждый день, 00:00
c.AddFunc("@midnight", cleanupSessions) // синоним @daily
c.AddFunc("@hourly", refreshCache) // в начале каждого часа
c.AddFunc("@every 1h30m", syncRates) // интервал, без привязки к часамВсе дескрипторы принимаются тем же AddFunc и тем же парсером — ParseStandard тоже их понимает, так что проверка выражений из конфига покрывает и этот случай. Синонимы @annually и @midnight даны ради читаемости: выбирайте одну форму и держите её по всему проекту, иначе конфиг превратится в коллекцию стилей.
@every отсчитывает интервал не по сетке часов, а от предыдущего срабатывания: первый запуск случится через полтора часа после старта планировщика, следующий — через полтора часа после него. Долгая задача сдвигает цепочку, а рестарт процесса переносит все моменты заново. Если расписание обязано совпадать с круглыми часами — например, отчёт «в начале часа» для сравнения с другими данными, — берите выражение с полем минут, а не @every.
Когда дескриптор, а когда выражение#
- Суточные и более редкие запуски в полночь — @daily и @weekly: читается мгновенно, ошибиться негде.
- Запуск «в начале часа» — @hourly, если вам не нужен контроль над минутой.
- Неизвестный заранее интервал из конфига — @every с Duration: парсинг расписания не нужен.
- Конкретное время, шаги, окна и дни недели — только пять полей: дескриптор «в 09:30» не умеет.
- Смешивать стили в одном конфиге можно, но лучше выбрать один стиль расписаний на проект.
Проверка: привязка шага
Процесс стартовал в 13:42. Когда выполнится задача с выражением */10 в поле минут?
Проверка: @every против поля часов
Чем @every 1h отличается от выражения с нулевой минутой каждый час?
Во сколько выполнится задача?
Прочитайте код и выберите, что он выведет:
c := cron.New()
_, err := c.AddFunc("@daily", nightlyReport)
if err != nil {
log.Fatal(err)
}
c.Start()
// процесс стартовал в 14:00
// во сколько выполнится nightlyReport?Соберите три расписания
Впишите звёздочку, диапазон будней и ключевое слово интервального дескриптора.
Соберите регистрацию полечасовой проверки
Расставьте строки в порядке выполнения: от создания планировщика до вечного ожидания в main.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: спецсимволы и дескрипторы
Планировщик — часть вашего процесса, поэтому код урока выполняется локально: модуль с robfig/cron/v3 из второго урока и один main.go. Песочница сайта умеет только подмножество стандартной библиотеки — fmt, strings, strconv, math, sort, errors и time — и сторонних пакетов в ней нет. Все выражения урока можно проверить интерактивами прямо на странице, а полночь @daily дождаться в браузере не выйдет.
Четыре спецсимвола и шесть дескрипторов закрывают почти весь словарь расписаний, который нужен сервису: шаги, окна, списки и читаемые сокращения. Дальше выражения станут параметром задач: в уроке про AddFunc разберём, какие функции годятся в задачи, как передавать им параметры через замыкания и почему тело задачи должно быть маленьким. Если хочется сразу увидеть дескрипторы в деле против сетки часов — загляните в урок про сравнение с Ticker, там эта разница решает выбор инструмента. Про секунды и шестипольные конфиги из старых статей — урок про NewWithSeconds. В домашнем задании — перевод человеческих формулировок в выражения со спецсимволами и проверка себя через ParseStandard.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Звёздочка — весь диапазон поля; слэш — шаг от начала поля, а не от старта процесса.
- Дефис даёт диапазон включительно, запятая — список; в месяцах и днях недели работают имена JAN-DEC и MON-SUN.
- Шаг вешается и на диапазон: 10-45/5 — это 10, 15, 20 и так до 45; форма N/шаг в robfig v3 означает N-максимум/шаг.
- Дескрипторы @yearly, @monthly, @weekly, @daily, @hourly — читаемые замены типовых выражений в зоне планировщика.
- @every — интервал Duration от предыдущего срабатывания: плывёт после рестартов и не привязан к круглым часам.
- Для отчётов и сравнимых данных выбирайте сетку часов; @every — для фоновой подметки без привязки ко времени суток.
Домашнее задание
Переведите пять человеческих формулировок в выражения: «каждые двадцать минут по будням с 8 до 20», «полночь воскресенья», «полдень и полночь по выходным», «окно с 10 по 40 минуту шагом в десять», «раз в сорок пять минут без привязки к часам». Проверьте все пять через cron.ParseStandard в отдельной программе, затем зарегистрируйте их в одном планировщике из урока и убедитесь по логу, что первые тики совпали с ожиданиями.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем @daily лучше выражения 0 0 * * *?
Только читаемостью: дескриптор разворачивается ровно в это выражение. В коде и конфигах @daily читается без декодирования полей, но как только появится время отличное от полуночи, дескриптор придётся заменить на пять полей — некоторые команды сразу пишут выражения ради единого стиля.
Почему шаг не совпадает с моментом запуска процесса?
Шаг отсчитывается по сетке значений поля от его начала: минуты с шагом десять всегда кратны десяти, независимо от того, когда поднялся планировщик. Это делает расписания синхронными между инстансами и рестартами. Интервальное поведение «каждые N минут от старта» реализует @every — но там сетка плывёт.
Можно ли задать шаг меньше минуты?
Не в стандартном парсере: пять полей заканчиваются минутами. Для секундного расписания планировщик создаётся с опцией секунд — cron.NewWithSeconds, и тогда первым полем становятся секунды. Помните, что частые тики — это постоянный дожим на зависимости: перед включением секунд оцените, что задача делает.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Логирование в Go: структурные логи на slog
Курс по slog в Go: уровни и атрибуты, JSON-логи, свой Handler, производительность, секреты и прод. 25 уроков с примерами и проектом.
- Логировать структурированно: уровни, ключи-атрибуты, группы
- Выбирать формат вывода: TextHandler, JSONHandler, свой Handler
- Мигрировать со старого пакета log без переписывания всей кодовой базы
- Держать производительность: Enabled, предвычисление, горячий путь