Содержание урока · 11 разделов
- tea.Batch: параллельный запуск команд
- tea.Sequence: команды по очереди
- Batch против Sequence
- Ловушка: порядок в Batch ничего не значит
- Вложенность: комбинации внутри комбинаций
- Fan-out: команда на каждый элемент
- Гонки и детерминизм: как думать о параллельности
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Init возвращает ровно одну команду — это ограничение интерфейса, и на старте оно кусается. Программе нужно одновременно: загрузить задачи, поднять спиннер, проверить настройки, запустить часы. Возвращать их по одной невозможно, а выбирать «самое важное» — значит халтурить. Update попадает в ту же ловушку: пользователь нажал Enter, и программе нужно и отправить форму, и включить индикатор. Ответ — комбинаторы: tea.Batch и tea.Sequence принимают любое число команд и возвращают одну составную, которая честно исполняет все. Оба появились не для красоты: одиночной команды хватает ровно до тех пор, пока приложение не начало по-настоящему работать с внешним миром. Разница между ними — в порядке и одновременности, и разница эта дороже, чем кажется: от неё зависит, упадёт ли ваш экран запуска на медленной сети. Это урок о том, как планировать много асинхронной работы сразу — и не доверять порядку её выполнения.
tea.Batch: параллельный запуск команд#
tea.Batch(cmds ...Cmd) — вариадическая функция: принимает сколько угодно команд и возвращает одну. Фреймворк разворачивает её и запускает все команды одновременно, каждую в своей горутине. Сообщения приходят в Update по мере завершения: какая работа закончилась раньше, то сообщение и прилетело первым. Никакой синхронизации между командами нет — Batch это чистый fan-out: раздал задачи, собрал результаты. Идеальный клиент — независимые операции: загрузка настроек, тик спиннера, проверка версии на сервере. Ни одна не ждёт другую, значит, параллельность только ускорит старт. Механика честная: изнутри Batch возвращает команду со списком остальных, которую фреймворк разворачивает рекурсивно, — для вас это невидимо и не меняет правил игры. Ключевое следствие, которое стоит выучить наизусть: Batch не даёт никаких гарантий порядка сообщений. Ни «первая в списке придёт первой», ни «порядок стабильный между запусками». Первым придёт тот, кто закончил, — и точка. Всё, что построено на предположениях об этом порядке, построено на песке; почему — увидим на примере ниже.
Старт программы пачкой
GOfunc (m model) Init() tea.Cmd {
return tea.Batch(
m.spinner.Tick, // тикающий спиннер
loadTasksCmd(), // задачи из хранилища
loadSettingsCmd(), // настройки
)
}Три независимые операции запущены одновременно. Тики спиннера придут постоянно, результаты загрузок — как закончатся; Update примет их по одному обычным type-свитчем. Ни одна загрузка не блокирует анимацию, и наоборот — именно так выглядит живой старт программы.
tea.Sequence: команды по очереди#
tea.Sequence(cmds ...Cmd) запускает команды последовательно: следующая стартует только после того, как сообщение предыдущей дошло до Update. Это конвейер с очередью шагов, и он нужен всюду, где порядок — часть смысла. Классический пример — протокол: сначала connectCmd, потом authCmd, потом loadTasksCmd. Запускать их пачкой бессмысленно: авторизация без установленного соединения просто упадёт, и вы получите три ошибки вместо одной. Sequence превращает этот список в детерминированную цепочку: каждый шаг гарантированно видит результат предыдущего — потому что он уже обработан в Update и разложен по модели. Плата за порядок — скорость: суммарное время равно сумме шагов, параллельности нет. Поэтому правильный вопрос при выборе — не «что быстрее», а «зависит ли шаг от предыдущего». Зависит — Sequence. Не зависит — Batch. Sequence выручает и в сценариях онбординга: показать приветствие, дождаться подтверждения, загрузить первый экран — тоже цепочка шагов. Комбинации обоих закрывают остальные случаи.
Протокол запуска по шагам
GOfunc (m model) Init() tea.Cmd {
return tea.Sequence(
connectCmd(), // 1. установить соединение
authCmd(), // 2. авторизоваться
loadTasksCmd(), // 3. загрузить данные
)
}Каждая команда стартует после доставки сообщения предыдущей. Ошибка на шаге обрывает цепочку там, где случилась: если connectCmd вернёт сообщение с ошибкой, authCmd не запустится, — обрабатывайте её в Update так же, как обрабатывали бы одиночную команду.
Batch против Sequence#
| Свойство | tea.Batch | tea.Sequence |
|---|---|---|
| Запуск | все команды сразу | по одной, строго по порядку |
| Порядок сообщений | не гарантирован | предсказуем: шаг за шагом |
| Скорость | как самая долгая команда | как сумма всех команд |
| Зависимости между шагами | недопустимы | разрешены и поддержаны |
| Типичное применение | независимые загрузки, тики | протоколы, миграции, сценарии |
Ловушка: порядок в Batch ничего не значит#
Самая частая ошибка с Batch выглядит невинно. В Init возвращают Batch(loadConfigCmd(), loadTasksCmd()), а в Update ловят сообщение настроек раньше сообщения задач и строят на этом логику: «настройки пришли первыми, применю их до отрисовки». На машине разработчика с локальной базой это работает месяцами. Потом программа уезжает к пользователю с медленной сетью, настройка начинает приходить позже задач — и экран стартует с пустым профилем. Я ловил именно такой баг в панели мониторинга: две команды в Batch, на моей машине порядок был стабильным год, а у коллеги в другом дата-центре — противоположным. Диагностика мучительная, потому что баг «плавает». Лечение — принципиальное: Batch годится только для независимых команд, а если B не может работать без результата A, между ними Sequence или второй заход через Update. Никогда не угадывайте порядок — проектируйте код, которому порядок безразличен.
Если команда B использует результат команды A, кладите их в Sequence — или запускайте B из Update после обработки сообщения A. Batch запустит B до того, как A закончила, и вы получите гонку, которая проявляется раз в неделю.
Вложенность: комбинации внутри комбинаций#
Batch и Sequence возвращают обычный tea.Cmd — значит, их можно скармливать друг другу и смешивать со своими командами на любом уровне. Механика честная: BatchMsg изнутри разворачивается фреймворком рекурсивно, и глубже одного-двух уровней уходить обычно не приходится. Живой шаблон старта: Sequence снаружи, потому что этапы зависят друг от друга, а внутри этапа — Batch из независимых задач. Так вы получаете и порядок фаз, и параллельность внутри фазы. Обратная комбинация — Batch из последовательностей — встречается реже, но уместна, когда несколько независимых протоколов идут параллельно, и каждый — по шагам. Полезная привычка: читать такие вложенности как дерево, где ветка Sequence исполняется вертикально, а ветка Batch — веером. Если дерево в голове не собирается, код для читающего соберётся ещё меньше — дробите на именованные функции-команды.
Смешанный старт: порядок фаз, параллельность внутри
GOfunc (m model) Init() tea.Cmd {
return tea.Sequence(
tea.Batch(
m.spinner.Tick,
loadSettingsCmd(),
),
authCmd(),
tea.Batch(loadTasksCmd(), loadStatsCmd()),
)
}Первым стартует веер: тики спиннера плюс настройки параллельно. После доставки их сообщений начинается authCmd, и только после неё — вторая пачка: задачи и статистика. Порядок фаз детерминирован, внутри фаз — честная параллельность.
Fan-out: команда на каждый элемент#
Второй шаблон, где Batch незаменим, — одинаковая работа по списку. Десять адресов, которые нужно пропинговать; двадцать файлов, которые нужно прочитать; пакетный индекс, который надо обновить по одному элементу. Решение механическое: в цикле сгенерируйте срез команд и отдайте его в tea.Batch пачкой. Фабрика-замыкание из пятого урока здесь главное действующее лицо: каждый элемент списка захватывается своим замыканием, и команды не путаются между собой, а ошибка элемента возвращается его же командой — сразу понятно, кто виноват. Сотни одновременных команд фреймворку не страшны — горутины дешёвые. Страшна цель: если адресатов тысячи, подумайте о пуле ограничений — слишком много одновременных соединений упрутся в лимиты сервера или самого терминала по отрисовке сообщений. Простой вариант — нарезать список на куски и планировать следующий Batch из Update, когда предыдущий отчитался. А материал для настоящих сетевых fan-out'ов ждёт в уроке про HTTP-данные.
Гонки и детерминизм: как думать о параллельности#
Параллельность пугает, если привыкли к потокам с блокировками. В Bubble Tea гонок между вашими командами почти не бывает — и вот почему. Команды не делят состояние: каждая работает со своими аргументами и возвращает сообщение, а всё состояние меняет один-единственный Update, который обрабатывает сообщения строго по одному. Пока вы держитесь этого правила elm архитектуры, параллельный Batch безопасен по построению: каким бы ни был порядок, каждая обработка видит целостную модель. Проблемы начинаются не от параллельности, а от ожиданий: код, который предполагает порядок прибытия или однократность события, — уже не конкуренция за память, а логическая гонка, и таблица Batch против Sequence выше — про неё. Противоядие то же: пишите обработчики так, будто каждый факт может прийти в любой момент и даже дважды — идемпотентно и без предположений о соседях. Тогда скорость пачки достаётся бесплатно, а редкие порядки превращаются из багов в просто события.
Комбинации команд — библиотечная механика, в песочнице она не выполняется: запустите примеры локально, через go run. Хотите увидеть порядок сообщений своими глазами? Добавьте в модель поле log []string, в каждой ветке Update дописывайте в него маркер события и печатайте журнал внизу View — во время работы TUI печатать в терминал нельзя, а такой журнал честно покажет порядок без единого fmt.Println.
Проверка: семантика Sequence
Чем tea.Sequence отличается от tea.Batch?
Проверка: порядок сообщений
В Batch положили loadConfigCmd и loadTasksCmd. Что можно предполагать о порядке их сообщений в Update?
Какое сообщение придёт первым?
Прочитайте код и выберите, что он выведет:
type doneMsg struct{ tag string }
func fastCmd() tea.Cmd {
return func() tea.Msg {
time.Sleep(50 * time.Millisecond)
return doneMsg{tag: "быстрая"}
}
}
func slowCmd() tea.Cmd {
return func() tea.Msg {
time.Sleep(2 * time.Second)
return doneMsg{tag: "медленная"}
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(slowCmd(), fastCmd())
}Заполните пропуски в комбинациях
Восстановите выбор комбинатора: независимые задачи — пачкой, протокол — по шагам.
Соберите вложенный старт
Расставьте строки возвращаемого выражения Init: порядок фаз с параллельностью внутри.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: словарь комбинаций
Что дальше#
Комбинаторы закрыли дыру «одна команда на событие» — и попутно показали, что модель умеет разрастаться. Когда загрузок и панелей становится много, Update начинает нуждаться в порядке не меньше, чем Init: восьмой урок про композицию подмоделей учит делить экран и сообщения между частями программы. Тики часов и таймауты — те же команды, собранные через Batch, — разбирает урок про tea.Tick и таймеры. А если хотите освежить одиночные команды, вернитесь к пятому уроку или откройте страницу курса Bubble Tea — там вся программа из 25 уроков. Практика на сегодня: возьмите старт из примера с Sequence, добавьте четвёртый этап loadLogsCmd и замерьте локально, как меняется время до первого кадра, если переставить фазы местами.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный терминал
модель Bubble Tea · Model → Update → ViewОтредактируйте модель, Update или View — и нажмите «Собрать модель»: код разберётся и начнёт отвечать на клавиши, как настоящий tea.Program.
Клавиатура — обработано в Update
модель собрана: settings int = 0 · tasks int = 0 · pending int = 2 · log []string (0)
программа работает — нажмите клавишу ниже
Коротко: что запомнить
- tea.Batch(cmds...) запускает команды параллельно; порядок сообщений не гарантирован.
- tea.Sequence(cmds...) исполняет команды по очереди: следующий шаг ждёт доставки сообщения предыдущего.
- Зависимые команды кладут в Sequence; независимые — в Batch; смешивать можно и нужно.
- Предположение о порядке сообщений из Batch — плавающий баг, который живёт месяцами.
- Комбинаторы возвращают обычный tea.Cmd и вкладываются друг в друга.
- Fan-out по списку — цикл с фабрикой замыканий и Batch(cmds...).
Домашнее задание
Соберите стартовую цепочку: Sequence из двух фаз, внутри первой — Batch из спиннера и настроек, внутри второй — Batch из задач и статистики. Замедлите каждую команду своим time.Sleep и запустите локально. Запишите порядок прихода сообщений, потом поменяйте команды местами внутри пачек и повторите: убедитесь, что фазы не перепутались, а порядок внутри пачек — плавает.
Личные заметкиПусто
Вопросы и ответы по уроку
Что вернёт tea.Batch, если одна из команд упадёт с ошибкой?
Batch ничего не агрегирует: ошибка придёт в Update обычным сообщением той команды, которая упала, остальные продолжат работать. Это ещё один довод не полагаться на порядок — сбои перемешивают его окончательно.
Можно ли вкладывать Batch в Sequence и наоборот?
Да: оба комбинатора возвращают обычный tea.Cmd, поэтому вложенность любой глубины собирается компилируемо. Практичный потолок — два уровня: Sequence фаз с Batch внутри каждой, иначе дерево команд перестаёт читаться и тестироваться.
Сколько команд выдержит один Batch?
Сотни — без вопросов: под каждую команду фреймворк заводит горутину, а они дешёвые. Ограничение обычно не в bubbletea, а во внешних ресурсах: лимитах соединений, rate limits API и способности Update переварить лавину сообщений.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Zap: структурное логирование на Go
Интерактивный курс Zap — быстрого логгера для Go: уровни, поля, JSON, ротация, интеграции с Gin, GORM и gRPC. 25 уроков с примерами и итоговым проектом.
- Писать структурные логи с полями вместо строк
- Выбирать между Logger и SugaredLogger осознанно
- Настраивать энкодеры, выводы и ротацию файлов
- Разбираться в zapcore: Core, Encoder, WriteSyncer, LevelEnabler