Содержание урока · 12 разделов
- Один поток, один интерфейс
- Почему правило существует: гонка данных
- fyne.Do: мост обратно в главный поток
- Привязки и потоки: где проходит граница
- DoAndWait и цена ожидания
- Рецепты: таймер, сеть, отмена
- Каталог потоков и мостов
- Практика: чек-лист потоков приложения
- Что дальше: файлы без диалоговых блокировок
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Один поток, один интерфейс#
У приложения на Fyne есть поток-владелец: главный goroutine, который после ShowAndRun из каркаса второго урока перестаёт быть твоим и становится циклом событий. Он выбирает уведомления операционной системы, определяет, над каким объектом курсор или палец, вызывает твои колбэки и раз в кадр отрисовывает то, что помечено изменённым. С версии 2.6.0 договорённость ужесточилась: все события и колбэки тулкита исполняются в одном и том же goroutine — и обработчик кнопки, и слушатель настроек, и колбэк диалога из тринадцатого урока. Это даёт скорость и предсказуемость: внутри обработчика ты можешь спокойно читать и менять любые виджеты, гонок с самим собой не будет. Обратная сторона — твоя горутина, запущенная через go func(), живёт вне этого мира: для неё виджеты — чужая память без защиты. Русский запрос «fyne goroutines» — это вопрос о границе между этими двумя мирами и о мосте через неё. Граница простая: всё, что имеет отношение к экрану — SetText, Refresh, SetContent, Show, диалоги, заголовок окна, — живёт в главном потоке; всё, что умеет без экрана — сеть, файлы, расчёты, ожидания, — живёт где угодно. Мост — две функции из пакета fyne, о которых дальше.
Часы из горутины
GOpackage main
import (
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.NewWithID("io.gomaster.clock")
window := myApp.NewWindow("Часы")
output := widget.NewLabel("--:--:--")
output.TextStyle.Monospace = true
go func() {
ticker := time.NewTicker(time.Second)
for now := range ticker.C {
stamp := now.Format("15:04:05")
fyne.Do(func() {
output.SetText(stamp)
})
}
}()
window.SetContent(output)
window.ShowAndRun()
}Горутина с тикером живёт своей жизнью и раз в секунду готовит строку. Само обновление спрятано в fyne.Do: анонимная функция уезжает в очередь главного потока и исполняется там, где SetText безопасен. Это официальный паттерн обновления из фона — канонический пример часов из документации Fyne.
Что произойдёт, если убрать fyne.Do и вызвать output.SetText(stamp) прямо в горутине с тикером?
Почему правило существует: гонка данных#
Гонка данных — это когда два потока обращаются к одной ячейке памяти одновременно, и хотя бы один пишет. Для Label это выглядит невинно: горутина пишет в поле Text, а главный поток в тот же момент рисует виджет и читает то же поле. Что прочитает отрисовка — старую строку, новую или половину каждой — решает планировщик, и от запуска к запуску исход разный. Хуже того: запись строки не атомарна, и читатель может увидеть разорванное значение, а составные структуры — невалидную память. Для map и слайсов те же гонки кончаются фатальнее: рантайм Go умеет ловить одновременную запись в карту и убивает процесс. Дешёвого «поставить мьютекс на каждый виджет» в тулките нет намеренно: каждый вызов с локом раз за кадр на каждый объект из дерева — это умножение стоимости отрисовки на порядок, и ради этого Fyne выбрал контракт «один поток — один интерфейс» вместо повсеместных замков. Твоя страховка — детектор гонок: собери приложение через go run -race и гоняй сценарий с фоновыми задачами; детектор печатает стеки обоих участников спора. Он ловит только фактические совпадения по времени, поэтому сценарий стоит прогнать несколько раз и под нагрузкой — тишина одного прогона ничего не доказывает.
Порядок не гарантирован
Прочитайте код и выберите, что он выведет:
go func() {
fyne.Do(func() { output.SetText("готово") })
fmt.Println("поставил в очередь")
}()fyne.Do: мост обратно в главный поток#
Обе функции-моста появились в версии 2.6.0 и живут в самом пакете fyne. fyne.Do(fn) ставит функцию fn в очередь главного потока и немедленно возвращается; главный поток исполнит её между текущим моментом и следующим кадром — обычно в пределах десятка миллисекунд. Русский запрос «fyne поток gui» почти всегда про эту функцию. Правила гигиены очереди: держи функцию короткой — пока она исполняется, интерфейс не рисует и не принимает ввод; собирай изменения пачкой в один Do вместо десяти мелких — каждая функция это прогон через очередь; и не рассчитывай, что вызов исполнится мгновенно — до кадра ещё дойти надо. Внутри fn ты снова в главном потоке со всеми правами: можно и SetText, и Refresh, и показать диалог. Внизу лестницы есть и технический этаж: fyne.Do — обёртка над Driver().DoFromGoroutine(fn, wait), где второй параметр и есть выбор между асинхронным и синхронным поведением; разработчику почти никогда не нужно звонить туда напрямую — знай, что этаж существует. А вот пара более удобных входов сверху: колбэки, которые тулкит вызывает сам, — события шестого урока, слушатели настроек девятнадцатого — уже приходят из главного потока, и оборачивать их не нужно; мост нужен только для горутин, которые создал ты сам.
Загрузчик с прогрессом
GOpackage main
import (
"fmt"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.NewWithID("io.gomaster.loader")
window := myApp.NewWindow("Загрузка")
status := widget.NewLabel("Готов к работе")
bar := widget.NewProgressBar()
start := widget.NewButton("Начать", func() {
start.Disable()
go func() {
steps := 5
for i := 1; i <= steps; i++ {
time.Sleep(300 * time.Millisecond) // имитация работы
done := float64(i) / float64(steps)
fyne.Do(func() {
bar.SetValue(done)
status.SetText(fmt.Sprintf("Шаг %d из %d", i, steps))
})
}
fyne.Do(func() {
status.SetText("Готово")
start.Enable()
})
}()
})
window.SetContent(container.NewVBox(status, bar, start))
window.ShowAndRun()
}Обработчик кнопки делает три вещи: отключает кнопку, чтобы не запустить вторую загрузку параллельно, уходит в горутину и через fyne.Do докладывает о прогрессе. Пока горутина спит и работает, интерфейс живёт: окно двигается, кнопки отзываются. Заметь: значения i и done вычислены в горутине, а в очередь уезжает готовый результат — в замыкании нет живых гонных переменных.
Что из перечисленного можно вызывать из собственной горутины БЕЗ fyne.Do?
Привязки и потоки: где проходит граница#
Семнадцатый урок оставил жёлтую рамку с обещанием разъяснить — выполняем. Источник привязки хранит значение под мьютексом: Get безопасен откуда угодно. Но Set запускает каскад: слушатели получают DataChanged в той же горутине, где случился Set, и если среди слушателей виджеты — их обновление уедет из твоей горутины прямиком в нарушение правила. Поэтому формула канона: писать в источники из фона — только через fyne.Do(func() { src.Set(v) }); тогда и значение, и весь каскад разъезжаются из главного потока, как будто пользователь нажал кнопку. Это же правило спасает DataListener из семнадцатого урока: колбэк слушателя исполняется в горутине Set, так что фоновая запись без обёртки тихо превратит твой логгер в гонщика. Полностью асинхронная альтернатива — не пускать фоновые данные напрямую в источники, а передавать их каналом и читать в главном потоке, но для типовых задач fyne.Do короче и читаемее. Отдельный случай — time.AfterFunc и тикеры: колбэк таймера исполняется в горутине рантайма, а не в главном потоке, так что таймер — тоже клиент fyne.Do. Проверь себя вопросом: кто создал goroutine, из которой вызван код? Если ответ «не тулкит» — нужен мост.
DoAndWait и цена ожидания#
fyne.DoAndWait(fn) — то же самое с одной разницей: вызывающая горутина ждёт, пока главный поток исполнит fn, и возвращается уже после обновления. Документация описывает окно исполнения честно: между моментом запроса и следующим кадром, то есть до ~15 миллисекунд ожидания. Когда ожидание оправдано? Когда следующий шаг горутины зависит от факта обновления: ты обновляешь буфер изображения и не хочешь начать новый тик, пока старый не отрисован; тесту нужно знать, что интерфейс уже увидел значение; два последовательных Do должны прийти строго в порядке написания кода. За это платишь синхронностью: горутина стоит, а главный поток в этот момент не рисует. И главное предостережение: DoAndWait — инструмент только для своих горутин. Если позвать его из обработчика кнопки, то есть из главного потока работающего приложения, получится классический дедлок: главный поток ждёт очередь, которую обслуживает он сам, а обслуживать её он сможет только выйдя из твоего обработчика. Окно замрёт намертво. Единственное исключение — вызов до старта цикла событий, когда главный поток ещё твой: тогда функция исполнится сразу, но в этом сценарии обычно ещё некому и городить горутины.
Ожидание, которое вечно
Прочитайте код и выберите, что он выведет:
// в работающем приложении
start.OnTapped = func() {
fyne.DoAndWait(func() { output.SetText("шаг 1") })
fmt.Println("дождались")
}Рецепты: таймер, сеть, отмена#
Три сценария покрывают девять из десяти бытовых задач. Таймер: time.NewTicker в горутине и fyne.Do на каждый тик — часы, автосохранение раз в минуту, обновление курсов; на выходе из приложения тикер стоит остановить — окно закрылось, а горутина продолжает тикать вхолостую, храни stop-канал или контекст. Сеть: обработчик кнопки дёргает go func(), внутри — запрос стандартным http.Get, ответ разбираешь в горутине, а в главный поток уезжает уже готовая строка или структура пачкой в одном fyne.Do; не гоняй сырые байты через очередь по кусочкам — одна функция, одно обновление. Отмена: длинная задача обязана уметь останавливаться — context.WithCancel из обработчика кнопки «Стоп», select в рабочем цикле на ctx.Done(), по завершении fyne.Do со статусом; горутина, которую нельзя остановить, переживает окно и портит следующий запуск. Что выносить не стоит: всё, что быстрее пары миллисекунд, — мост дороже самой работы; событие — быстрый обработчик — Refresh, пропорция из шестого урока, не отменяется. Что выносить обязательно: чтение больших файлов, фильтрацию тысяч строк, что угодно с time.Sleep, сетевые вызовы — всё, за что пользователь платит замороженным окном.
Отмена по контексту
GOpackage main
import (
"context"
"fmt"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
func main() {
myApp := app.NewWithID("io.gomaster.worker")
window := myApp.NewWindow("Фоновая задача")
status := widget.NewLabel("Задача не запущена")
start := widget.NewButton("Запустить", nil)
stop := widget.NewButton("Остановить", nil)
start.OnTapped = func() {
ctx, cancel := context.WithCancel(context.Background())
start.Disable()
stop.Enable()
go func() {
for {
select {
case <-ctx.Done():
fyne.Do(func() {
status.SetText("Остановлено")
start.Enable()
stop.Disable()
})
return
case <-time.After(time.Second):
fmt.Println("порция работы выполнена")
}
}
}()
stop.OnTapped = func() {
cancel()
}
}
window.SetContent(container.NewVBox(status, start, stop))
window.ShowAndRun()
}Каждый запуск получает свой контекст и своё отменяющее замыкание. Рабочий цикл раз в секунду проверяет ctx.Done() — и по отмене возвращается в главный поток, чтобы обновить статус и кнопки. Горутина умирает до конца, ни одна её ветка не трогает виджеты напрямую. В реальном приложении вместо печати в консоль была бы порция настоящего дела.
Где исполняется функция, переданная в fyne.Do?
Каталог потоков и мостов#
| Объект | Что это | Как звать из своей горутины |
|---|---|---|
| Главный поток GUI | горутина цикла событий после ShowAndRun | не звать вовсе — это твой дом, только мостами |
| Колбэки виджетов и событий | Tapped, OnChanged, колбэки диалогов | исполняются в главном потоке — интерфейс трогай напрямую |
| fyne.Do(fn) | асинхронная постановка в очередь главного потока | базовый мост для обновления UI |
| fyne.DoAndWait(fn) | то же с ожиданием исполнения | когда следующий шаг зависит от обновления; никогда из колбэков |
| Driver().DoFromGoroutine(fn, wait) | механизм под обе обёртки | почти никогда — fyne.Do короче и читаемее |
| Preferences (myApp.Preferences()) | хранилище настроек с внутренним мьютексом | Set и чтения безопасны напрямую |
| binding Get | чтение значения источника | безопасно из любой горутины |
| binding Set | запись, запускающая каскад слушателей | обёртывай в fyne.Do — канон семнадцатого урока |
| go run -race | детектор гонок данных | прогоняй каждый сценарий с горутинами |
| context.WithCancel | отмена фоновой задачи | кнопка «Стоп» пишет в контекст, задача умирает сама |
Собери мост для фонового отчёта
Восстанови четыре пропуска: запуск горутины, мост в главный поток, обновление метки и синхронный вариант того же моста.
Путь обновления из горутины до экрана
Расставь по порядку, что происходит, когда горутина с сетевым запросом обновляет метку.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Реальный цикл событий с горутинами в браузерной песочнице курса не исполняется — GUI требует настоящего окна. Собери часы и загрузчик по второму уроку и запусти их дважды: как есть и через go run -race. Один и тот же код без fyne.Do под детектором покажет гонку, о которой глаз на экране не догадался бы.
Дедлок номер один — fyne.DoAndWait из колбэка работающего приложения. Номер два — канал с небуферизованной отправкой в горутину, которую главный поток не читает, и наоборот: главному потоку нечего делать в ожидании чужой работы. Номер три — time.Sleep внутри обработчика: спит именно интерфейс. Все три выглядят одинаково для пользователя — окно-картинка, — но лечатся по-разному: мостом, буферизованным каналом с отбором и горутиной соответственно.
Практика: чек-лист потоков приложения#
Прежде чем выпускать приложение с горутинами, пройди по списку. Каждая длинная операция — сеть, файл, тяжёлый расчёт — запускается из обработчика через go func(), и ни одна не исполняется в самом обработчике дольше пары миллисекунд. Каждое касание экрана из фона идёт через fyne.Do, а пачки изменений сгруппированы в одну функцию. Каждая фоновая задача останавливаема: контекст или stop-канал, проверка в цикле, и по закрытию окна ничего не тикает впустую. Кнопки, запускающие горутины, отключаются на время работы — двойной запуск одной загрузки это не ускорение, а гонка с самим собой. Общий между горутиной и интерфейсом состояние либо передаётся значением через fyne.Do, либо защищено — sync.Mutex или атомарные типы, — и ни одна переменная не живёт в двух мирах без защиты. Прогон под go run -race выполнен на сценарии с реальной нагрузкой, а не на пустом окне. И последнее: не плодить горутин без нужды — тикер, который обновляет подпись раз в минуту, можно заменить проверкой при событии; каждый лишний параллельный агент это будущая гонка, которую кто-то однажды будет ловить под детектором.
Что дальше: файлы без диалоговых блокировок#
Теперь приложение многопоточно в обе стороны: интерфейс отзывчив, фоновые задачи возвращаются мостом, отмена встроена в каждую долгую операцию. Опора урока — шестой с правилом главного потока и пропорцией «событие — быстрый обработчик — Refresh», семнадцатый с жёлтой рамкой про Set из чужой горутины, тринадцатый с диалогами, которые тоже живут в главном потоке, и второй с каркасом ShowAndRun. Следующая тема — файлы: хранилище приложения fyne.Storage, URI со схемой file:// и диалоги выбора, чтение которых ты теперь обязан оборачивать в горутину, если файл большой. Канон склейки уже готов: открываешь файл — уходи в горутину, возвращаешься — через fyne.Do. Полный маршрут — на странице курса Fyne.
Термины урока
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Виртуальный экран
HTML-модель виджет-дерева fyneФоновая горутина считает…
Экран собран: «fyne.Do и главный поток»
Коротко: что запомнить
- У приложения один поток-владелец: после ShowAndRun главный goroutine крутит цикл событий, и с версии 2.6.0 все колбэки тулкита исполняются именно в нём.
- Обновление UI из своей горутины без моста — гонка данных: исход непредсказуем, от удачи до падения; детектор go run -race обязателен для таких сценариев.
- fyne.Do(fn) — асинхронный мост: функция уезжает в очередь главного потока и исполняется до следующего кадра; собирай изменения пачками и держи функции короткими.
- fyne.DoAndWait(fn) ждёт исполнения — для случаев, когда следующий шаг зависит от обновления; из колбэков работающего приложения это дедлок.
- Preferences защищены мьютексом и пишутся из любой горутины; привязки пиши через fyne.Do — каскад слушателей должен разъезжаться из главного потока.
- Три рецепта покрывают быт: тикер с fyne.Do на каждый тик, сетевой запрос с готовой пачкой в одном Do, отмена по context.WithCancel.
- Чек-лист: длинное — в горутину, экран — только через мост, задачи останавливаемы, кнопки отключаются, общее состояние защищено, -race прогнан под нагрузкой.
Домашнее задание
Собери менеджер фоновой обработки текста. Вход — многострочный Entry из девятого урока и три кнопки: «Статистика», «Искать», «Стоп». Кнопка «Статистика» уходит в горутину и считает, сколько в тексте слов, строк и символов, затем через fyne.Do обновляет три метки одним вызовом. Кнопка «Искать» ищет подстроку по тексту в горутине с прогрессом: разбей текст на куски по абзацам и после каждого обновляй ProgressBar через fyne.Do, чтобы окно оставалось живым. «Стоп» отменяет поиск через context.WithCancel, горутина обязана выйти и через fyne.Do пометить статус «Остановлено». Добавь часы в заголовок окна на time.NewTicker с fyne.Do — и не забудь остановить тикер, когда приложение закрывается, перехватив закрытие через SetCloseIntercept из второго урока. Проверь себя под детектором: запусти go run -race и понажимай кнопки на большом тексте; ноль предупреждений — цель. Ловушка для самопроверки: сначала сделай версию «Статистики» без горутины и замерь, на каком тексте окно начинает замирать; этот порог и есть твоя личная граница «быстро — в обработчике, дольше — в горутину».
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Fyne не защищает виджеты мьютексами, как другие библиотеки?
Замок на каждый вызов умножил бы стоимость отрисовки: за кадр виджеты читаются десятки раз. Вместо этого тулкит фиксирует контракт «один поток — один интерфейс» и даёт дешёвые мосты fyne.Do/DoAndWait, а колбэки сам доставляет в главный поток — так быстрее и предсказуемее.
Можно ли вызывать fyne.Do до старта цикла событий, например в main до ShowAndRun?
Можно, но бессмысленно: до запуска цикла главный поток ещё твой, и вызов из него исполнится сразу, без очереди. Мост нужен после ShowAndRun — когда главный поток занят циклом событий, а твой код живёт в собственной горутине.
Что безопаснее для передачи данных из фона: fyne.Do или канал?
Для обновления экрана — fyne.Do: короче и без шаблонного кода с каналом. Канал стоит брать, когда данных много и они текут потоком: горутина пишет в канал, главный поток читает в цикле и обновляет раз в N сообщений — так очередь интерфейса не забивается тысячами мелких функций.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Cobra: CLI-приложения на Go
Интерактивный курс Cobra — фреймворка для CLI на Go: команды, флаги, конфиги Viper, автодополнение. 25 уроков с примерами кода, тестами и итоговым приложением.
- Создавать CLI-приложения с деревом команд на Cobra
- Работать с аргументами, флагами и их привязкой к конфигу
- Управлять конфигурацией через Viper: файлы, env, приоритеты
- Писать хуки PreRun/PostRun и корректно обрабатывать ошибки