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

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

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

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

Первая программа: Model, Update, View

Триада bubbletea model update view — весь фреймворк в трёх словах: модель состояния, функция обновления и функция отрисовки. Соберём на ней настоящий счётчик: типизированный tea.Model, ветвящийся Update, строковый View и запуск через tea.NewProgram — с разбором каждой строки и типичных ошибок новичка.

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

Задача на сегодня звучит скучно, а ощущается отлично: программа, где ↑ увеличивает число, ↓ уменьшает, q — выходит. На первый взгляд это страница кода, на второй — весь курс в миниатюре: счётчик использует те же приёмы, что панели задач и дашборды, только деталей меньше. Мы соберём его с нуля, по строкам, и попутно зафиксируем три привычки, которые окупятся на больших программах: состояние живёт в одной структуре, все события проходят через один Update, отрисовка — чистая функция от состояния. Код урока нужно запускать локально: Bubble Tea работает только в настоящем терминале, песочница сайта показывает логику на виртуальном, а ощущение живого интерфейса даст только go run на вашей машине. Начнём с контракта, который придётся подписать фреймворку.

Интерфейс tea.Model: контракт в три метода#

Чтобы Bubble Tea мог управлять вашей программой, он должен знать три вещи: что сделать при старте, как отреагировать на событие и что нарисовать. За это отвечает интерфейс tea.Model — единственный интерфейс фреймворка, который вы реализуете напрямую. Внутри библиотеки он объявлен так:

Код

GO
type Model interface {
        Init() Cmd
        Update(Msg) (Model, Cmd)
        View() string
}

Три метода — и ни одного лишнего. Init возвращает стартовую команду Cmd (или nil), Update получает любое событие Msg и возвращает новую модель вместе с командой, View — строку для отрисовки. Заметьте: интерфейс ссылается на типы Cmd и Msg без пакетного префикса, потому что объявлен внутри самого пакета bubbletea. В вашем коде это будет tea.Cmd и tea.Msg.

Model: состояние без магии#

Модель — обычная структура, никакой магии наследования. Поля — ровно то, что нужно интерфейсу: то, что рисуется, и то, что влияет на логику. Счётчику хватает одного поля count; у панели задач будут срез задач, индекс курсора и режим экрана. Реализация интерфейса неявная: достаточно объявить методы с нужными сигнатурами на своём типе — плюс в копилку Elm-архитектуры на Go, никакой церемонии регистрации. Отдельное слово о приёмниках: мы объявляем методы на значении, не на указателе, — и это осознанный выбор. Каждый Update возвращает модель как значение, поэтому состояние явно пересобирается на каждое событие, а не мутирует в глубине. Модель счётчика копировать дёшево; когда дойдёте до тяжёлых моделей, приёмник на указателе оправдан — но начните со значения, оно прощает ошибки. Отдельный бонус такого подхода — тесты: не зависящий от фреймворка Update можно звать в обычном unit-тесте с готовым сообщением и сравнивать модель с ожидаемой, без терминала и моков.

Init(): что происходит при старте#

Init вызывает один раз, до первого кадра. Его работа — объявить, какие действия нужны программе сразу: вернуть nil, если ничего, или команду tea.Cmd, если что-то. Команда — это отложенное действие: функция, которую фреймворк запустит в отдельной горутине, а её результат потом придёт в Update сообщением. Примеры: tea.SetWindowTitle("Счётчик") — попросить терминал сменить заголовок окна; загрузка начальных данных с сервера; старт тикания спиннера. Пока мы возвращаем nil: клавиши сами придут, когда пользователь нажмёт. Полноценное знакомство с командами — тема пятого урока курса, там же объясняется, почему Update никогда не блокируется. Сейчас важен порядок старта: Init → первый View → цикл событий. Если Init возвращает nil, программа просто рисует модель и замолкает до первого нажатия.

Модель, Init и Update: разбор по строкам

GO
package main

import (
        "github.com/charmbracelet/bubbletea"
)

type model struct {
        count int
}

func initialModel() model {
        return model{count: 0}
}

func (m model) Init() tea.Cmd {
        return nil
}

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
        switch msg := msg.(type) {
        case tea.KeyMsg:
                switch msg.String() {
                case "up":
                        m.count++
                case "down":
                        m.count--
                case "q", "ctrl+c":
                        return m, tea.Quit
                }
        }
        return m, nil
}

model — состояние: одно числовое поле. initialModel — конструктор со стартовыми значениями: он пригодится при запуске. Init возвращает nil — стартовых действий нет. Update — сердце программы: тип-свитч перехватывает все сообщения, case tea.KeyMsg отфильтровывает нажатия клавиш, вложенный свитч по msg.String() разбирает, какая именно клавиша. up увеличивает счётчик, down уменьшает, q и ctrl+c возвращают команду tea.Quit — сигнал фреймворку завершить цикл. Последняя строка возвращает модель без изменений для всех прочих событий.

Update: один switch вместо колбэков#

Update — единственное место, где состояние меняется. Это и есть elm архитектура go-фреймворка: никаких регистрируемых обработчиков, никаких замыканий над чужими полями. Каждое событие — клавиша, таймер, ответ сети, resize окна — превращается в значение tea.Msg и попадает в один и тот же switch. Конструкция switch msg := msg.(type) — тип-свитч: в каждой ветке переменная msg имеет конкретный тип, поэтому к её полям можно обращаться без приведения. Событие, которое вы не обработали, просто пролетает в финальный return m, nil — программа не падает, а молча игнорирует его. Это одновременно фича и ловушка: опечатка в строке клавиши не вызовет ни ошибки компиляции, ни паники, — ветка просто не сработает. В четвёртом уроке разберём клавиши так, чтобы опечаток не осталось. Пока держите в голове: любое поведение программы прослеживается от одного switch, и это делает отладку предсказуемой. Здесь же живёт важный контракт: сообщение, которое никто не перехватил, — не исключение и не ошибка, а штатная ситуация, и знать это нужно прежде, чем добавлять в Update собственные типы сообщений.

View: строка, которую перерисуют#

View — самая необязательная и самая благодарная часть. Фреймворк вызывает её после каждого Update и печатает возвращённую строку вместо предыдущей. Внутри — никакого состояния, никакой логики: только форматирование полей модели. Строку собирают fmt.Sprintf, многострочность — обычные \n; на большой программе этой суммы хватать перестанет и в дело пойдёт Lip Gloss с рамками и цветами, но сейчас достаточно чистого Sprintf. Важно понимать, когда View не вызывается: если Update вернул модель без изменений, фреймворк всё равно перерисует строку — она может зависеть от чего угодно, поэтому перерисовка после каждого события честнее и проще, чем отслеживание «а изменилось ли». Дорого? На практике дешевле, чем кажется: обновляются только изменившиеся строки. Если View начнёт менять модель или читать глобальные переменные — вы сломали elm архитектуру, и отладка потеряет опору. Держите View чистым. Последняя деталь: возвращать пустую строку тоже можно — фреймворк нарисует пустой кадр, чем иногда пользуются при переходах между экранами, но для счётчика пустой View — баг, а не фича.

View и main: сборка и запуск

GO
func (m model) View() string {
        return fmt.Sprintf(
                "Счётчик: %d\n\n"+
                        "↑ больше, ↓ меньше, q — выход\n",
                m.count,
        )
}

func main() {
        p := tea.NewProgram(initialModel())
        if _, err := p.Run(); err != nil {
                fmt.Println("Программа упала:", err)
                os.Exit(1)
        }
}

View складывает значение и подсказку в одну строку через Sprintf — пустые строки между ними делают вывод читаемым. В main две строки по делу: tea.NewProgram принимает начальную модель и возвращает программу, Run запускает цикл событий и блокирует main до выхода. Run возвращает финальную модель и ошибку; ошибку обязательно проверяем — если терминал недоступен, именно здесь вы узнаете об этом. Финальную модель пока отбрасываем подчёркиванием: что с ней делать — тема третьего урока.

Цикл: сообщение → Update → View#

Сложите три метода в одну картинку. Программа стартует, Init объявляет стартовые действия, View рисует первый кадр. Дальше — вечный цикл: фреймворк читает события терминала, заворачивает их в сообщения и передаёт в Update; Update возвращает новую модель; View рисует её; повторить. Это ровно та схема, по которой в Go гоняют данные между горутинами: получение значения из канала, обработка, отдача результата — если вы писали воркера на каналах из урока про каналы, этот цикл покажется старым знакомым. Разница в том, что фреймворк уже написал весь скучный код: чтение ввода, разбор клавиш, отрисовку и восстановление терминала при выходе. Программа завершается, когда Update возвращает команду tea.Quit либо когда Run падает с ошибкой — после выхода терминал возвращается в исходное состояние, и main продолжает работать как обычная программа. Скорость цикла на глаз неуловима: между нажатием и перерисовкой проходят миллисекунды, поэтому интерфейс ощущается живым, хотя внутри каждый кадр проходит через те же три метода, что и самый первый.

Три привычки, которые окупятся на больших программах#

  1. Держите модель маленькой: поле попадает в структуру, только если оно рисуется или влияет на логику — остальное живёт вне модели
  2. Не меняйте состояние вне Update: побочные эффекты — в команды tea.Cmd, о которых подробно расскажет пятый урок
  3. Держите View чистым: только поля модели и форматирование, без мутаций и глобальных переменных — иначе потеряете главную опору отладки
  4. Проверяйте каждую ветку Update на return — компилятор ловит не все забытые возвраты, а паника с nil приходит в самый неподходящий момент
  5. Называйте сообщения по смыслу события, а не по источнику: taskDoneMsg понятнее, чем channelMsg
  6. Один тип-свитч на модель: когда модель распухнет, вы разложите её на подмодели со своими свитчами — этому посвящён восьмой урок

Проверка: сигнатура Update

Что возвращает метод Update и зачем второй элемент?

Проверка: Init

Что означает Init(), возвращающий nil?

Прогоните счётчик в голове

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

type model struct{ count int }

func initialModel() model { return model{count: 5} }

// Update: "up" → m.count++, "down" → m.count--,
//         "q" → return m, tea.Quit

func (m model) View() string {
        return fmt.Sprintf("Счётчик: %d\n", m.count)
}

// Пользователь нажал: ↑, ↑, ↓, ↑

Заполните пропуски в Update

Восстановите разбор клавиш: тип сообщения, метод получения строки, инкремент и команду выхода.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.: switch msg.() { case "up": m.count case "q": return m, tea. } } return m, nil }

Соберите View из строк

Расставьте строки метода View в правильном порядке — от сигнатуры до закрывающей скобки.

Строки

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

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

Карточки: триада и её окружение

Термин

tea.Model

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

Интерфейс из трёх методов: Init() tea.Cmd, Update(tea.Msg) (tea.Model, tea.Cmd), View() string. Реализуется неявно — просто объявите методы.

1 / 6

Что дальше#

Счётчик собран — и уже полезен, но запускается голым: без полного экрана, без мыши, без связи с внешним миром. Третий урок про tea.Program закрывает именно это: опции вроде WithAltScreen и WithMouseCellMotion, отправка сообщений снаружи через p.Send и полный жизненный цикл от NewProgram до выхода из Run. Потом — команды tea.Cmd: как не блокировать Update и ходить в сеть, пока интерфейс живёт. Если хотите закрепить счётчик руками, запустите его локально и добавьте ветку case "r": сброс на ноль — одна строка, а цикл «поменял Update → увидел поведение» прогоните целиком. Напомню на всякий случай: полный код курса выполняется локально, нужен Go и настоящий терминал — и этого достаточно.

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

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

Виртуальный терминал

модель Bubble Tea · Model → Update → View

Отредактируйте модель, Update или View — и нажмите «Собрать модель»: код разберётся и начнёт отвечать на клавиши, как настоящий tea.Program.

виртуальный терминал · bubbleteaalt-screen
Счётчик: 0
↑/↓ или +/− — изменить, enter — сброс, q — выход

Клавиатура — обработано в Update

Журнал программы

модель собрана: count int = 0

программа работает — нажмите клавишу ниже

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

  • tea.Model — интерфейс из трёх методов: Init, Update, View; реализация неявная, достаточно сигнатур.
  • Model — обычная структура: поля только те, что рисуются или влияют на логику.
  • Update — единственное место изменения состояния: тип-свитч по сообщениям, каждая ветка возвращает (модель, команду).
  • Init с nil означает «стартовых действий нет»; ненулевой Cmd начинает фоновую работу сразу.
  • View — чистая функция от модели: строка на Sprintf, перерисовывается после каждого Update.
  • Вернуть nil из Update — паника «Model returned nil from Update!»: проверяйте каждый return.
  • Цикл «сообщение → Update → View» — тот же приём, что воркер на каналах; фреймворк уже написал скучную часть.

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

Соберите счётчик локально и расширьте его: добавьте клавишу r для сброса на ноль и клавишу s, которая записывает в модель строку статуса. Затем сломайте программу двумя способами: верните nil из одной ветки Update и уберите финальный return m, nil. Запишите, как именно ведёт себя программа в каждом случае — компилятор, паника или молчание.

Личные заметкиПусто
Хранятся только в вашем браузере (Bubble Tea · Первая программа: Model, Update, View)

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

Обязательно ли называть тип модели model?

Нет, имя произвольное — model просто традиция туториалов Bubble Tea. В реальных проектах типы называют по экрану или назначению: taskListModel, authModel. Важно не имя, а набор методов Init, Update, View с правильными сигнатурами.

Почему Update принимает и возвращает модель, а не мутирует её?

Так работает Elm-архитектура: возврат нового значения делает каждое изменение состояния явным и проверяемым. Мутации из глубины колбэков — главная источник багов в классических GUI, здесь этот путь закрыт конструктивно.

Можно ли вызывать View самому?

Можно, но незачем: фреймворк вызывает View после каждого Update и сам решает, когда перерисовать терминал. Ручной вызов имеет смысл в тестах — сравнить строку с ожидаемой; в живой программе трогать вывод руками не нужно.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

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

Zap: структурное логирование на Go

Интерактивный курс Zap — быстрого логгера для Go: уровни, поля, JSON, ротация, интеграции с Gin, GORM и gRPC. 25 уроков с примерами и итоговым проектом.

  • Писать структурные логи с полями вместо строк
  • Выбирать между Logger и SugaredLogger осознанно
  • Настраивать энкодеры, выводы и ротацию файлов
  • Разбираться в zapcore: Core, Encoder, WriteSyncer, LevelEnabler