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

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

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

Middle ~22 мин 7 вопросов

Песочница Go в браузере: что внутри и как устроен запуск кода

Проблема онлайн-песочницы в том, что запустить код в браузере — это не то же самое, что запустить его на сервере. Код, который работает у вас, может вести себя иначе: другие файловые операции, нет сети, нет настоящих горутин в полном смысле, а главное — никакого «серверного» исполнения, которое можно было бы спрятать в фиче страницы.

На этом сайте задача решена так: настоящий интерпретатор Go собран в WebAssembly, запускается в отдельном Web Worker и выполняет код прямо в браузере. Ниже — разбор устройства: что внутри песочницы, какие у неё лимиты и что происходит при их превышении, что работает в браузере, а что нет, как устроены графические примеры на fyne и терминальные на bubbletea.

Порядок ответа прежний: сначала проблема, потом механизм, потом граница и пример. Главная мысль: песочница — это компромисс между «настоящим Go» и «кодом, который безопасно исполнять у недоверенного пользователя», и у каждой стороны компромисса своя цена.

Практика запуска кода — в базовом курсе Go, сложные примеры с каналами и дженериками — в продвинутом.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

1. Что внутри песочницы Go и как она устроена?

Проблема, которую решает песочница на сайте: дать человеку написать и запустить Go без установки, без сервера и без риска для чужой машины. Значит, исполнение должно происходить на стороне пользователя — в браузере.

Внутри три слоя. Первый — интерпретатор: взят traefik/yaegi, это интерпретатор Go, который выполняет исходный код, разбирая его в AST и вызывая функции через рефлексию. Второй — WebAssembly: yaegi собран как обычная программа Go под GOOS=js GOARCH=wasm, поэтому в браузере он работает как модуль wasm. Третий — Web Worker: модуль выполняется в отдельном потоке браузера, а страница общается с ним через сообщения.

Что даёт такая схема. Код не покидает браузер — на сервер он не отправляется вообще, и это не обещание маркетинга, а устройство. Пользователь может писать что угодно: сервер не выполняет и не проверяет его. Worker изолирован от интерфейса: если программа зависнет или съест цикл, страница останется отзывчивой, а воркер можно перезапустить.

Отдельная деталь — стандартная библиотека. Интерпретатору подключаются символы stdlib.Symbols, поэтому доступны fmt, strings, sort, time, math и большая часть остального. Это не «урезанный Go», а подмножество, объявленное явно, — и именно поэтому некоторые вещи в песочнице не работают, о чём пойдёт речь дальше.

Второй движок — резервный: упрощённый интерпретатор, написанный на TypeScript. Он включается автоматически, только если wasm-модуль недоступен, и у него меньше возможностей: порядок обхода карты отсортирован, range по строке идёт по байтам, метки и каналы не поддерживаются.

// Протокол: страница отправляет в worker исходник программы
// worker → main thread: { id, source }
// worker → main thread: { ok, output[], error, engine }

// Движок помечается в ответе:
// engine: "yaegi-wasm"  — настоящий Go в WebAssembly
// engine: "ts-fallback" — упрощённый интерпретатор

2. Какие лимиты у песочницы и что происходит при их превышении?

Проблема, которую лимиты решают: пользовательский код может зациклиться или напечатать миллион строк. Без ограничений это либо зависшая вкладка, либо память, съеденная вкладкой, — и виноват окажется сайт.

В песочнице два предела. Первый — лимит времени: программа выполняется не дольше трёх секунд. Применяется это на стороне интерпретатора: запуск идёт в отдельной горутине, а таймер отслеживает завершение. Если время вышло, рантайм возвращает ошибку «превышен лимит времени» вместе с тем выводом, который успел напечататься.

Второй — лимит вывода: не больше двух тысяч строк. Он срабатывает иначе, чем лимит времени: когда коллектор вывода видит, что лимит достигнут, он отдаёт накопленные строки хост-коду через специальный обработчик, и воркер сам себя закрывает. Это сделано потому, что работающий интерпретатор нельзя прервать снаружи — если программа в бесконечном цикле печатает, единственный способ остановить её — закрыть воркер целиком.

Что из этого следует для пользователя. Во-первых, «программа не успела» — это про лимит времени, а не про ошибку в коде: сообщение об этом отдельное и внятное. Во-вторых, вывод программы остаётся в том виде, в каком успел напечататься, — по обрезанному результату видно, что лимит сработал. Что стоит помнить при написании примеров для уроков: код, который печатает тысячи строк в цикле, в песочнице выглядит как ошибка. Это не баг песочницы, а напоминание о том, что примеры должны быть короткими.

// Контракт ошибок песочницы:
// "превышен лимит времени"
// "слишком много вывода: превышен лимит строк"

// Пример: программа печатает в бесконечном цикле
for {
    fmt.Println("вечно")
}
// → сработает лимит строк, воркер закроется, страница продолжит работать

3. Что в песочнице работает, а что нет: это полный Go?

Проблема: слово «Go в браузере» звучит как полный язык, и это ожидание стоит уточнить. В песочнице поддержано многое, но не всё, и у ограничений есть конкретные причины.

Что работает. Основной язык целиком: переменные, структуры, методы, интерфейсы, срезы, карты, замыкания, defer, обработка ошибок, горутины, каналы, select, дженерики, стандартная библиотека в объёме, подключённом через символы stdlib. То есть учебные примеры — сортировки, структуры, конкурентность, работа с JSON — выполняются как настоящий Go.

Что работает с оговорками. Пакет unsafe подключён частично: Pointer, Add, StringData и String доступны, а Sizeof, Alignof и Offsetof — заглушки, которые возвращают ноль и не должны вызываться напрямую. Приведение типизированного указателя к unsafe.Pointer интерпретатор выполнить не может: рефлексия отказывается на таком преобразовании, и это ограничение зафиксировано в текстах уроков.

Что не работает. Сетевые вызовы и файловая система в браузере недоступны в привычном виде: браузерная песочница не даёт файловой системы, а исходящие запросы ограничены политикой CORS. Пакеты с cgo не собираются: CGO в WebAssembly не поддерживается. Графические приложения требуют отдельной обработки — про это ниже.

Отдельно стоит сказать про порядок обхода карты: в настоящем Go он случайный, и в песочнице на настоящем движке тоже случайный, а вот в резервном упрощённом интерпретаторе карта обходится в отсортированном порядке.

Практический вывод: песочница годится, чтобы писать, запускать и отлаживать учебный код. Для задач с сетью, файлами и C-библиотеками нужна обычная среда.

// Что доступно с unsafe в песочнице:
// unsafe.Pointer   — тип (зарегистрирован)
// unsafe.Add       — работает
// unsafe.StringData — работает
// unsafe.String    — работает
// unsafe.Sizeof    — заглушка, возвращает 0

// Чего нет: cgo, произвольных файлов, прямых TCP-соединений

4. Почему для песочницы выбран интерпретатор, а не компиляция в WebAssembly?

Проблема: в браузере нужно выполнить произвольный код, написанный прямо сейчас, — то есть исходник, а не готовый бинарник. Это сразу исключает обычную компиляцию: скомпилировать код в wasm можно только на машине, которая его потом выполнит.

Отсюда решение: интерпретатор, который сам собран в WebAssembly. yaegi берёт исходник, разбирает его в AST, проверяет типы и выполняет через рефлексию. Появился этот путь не сразу: WebAssembly со спецификацией GC (в Go он называется wasm_gc) открыл возможность собрать в браузер код с управляемой памятью, и только после этого интерпретатор Go заработал в браузере осмысленно.

Что это даёт. Скорость запуска минимальна: не нужно ждать компиляцию — код выполняется сразу, как в обычном интерпретаторе. Время на компиляцию уходит на пользователя, а не на сервер.

Чем приходится платить. Скорость выполнения ниже, чем у скомпилированного кода: вместо машинных инструкций вызовы идут через рефлексию. Для учебных задач — сортировки, разбор структур, работа с коллекциями — разница незаметна, но для тяжёлых вычислений она ощутима. Второе ограничение — типы: интерпретатор проверяет их по своим правилам, и часть возможностей языка, связанных с unsafe и низкоуровневым кодом, там не работает.

Ответ на собеседовании: компиляция в wasm дала бы скорость, но потребовала бы сервера для сборки; интерпретатор в wasm даёт мгновенный запуск и приватность, и это осознанный размен.

// Три варианта и их цена:
// 1. Серверная компиляция в wasm — быстро, но нужен сервер и код уходит с устройства
// 2. Интерпретатор в wasm (yaegi) — мгновенный старт, код остаётся в браузере
// 3. Локальный Go → wasm через TinyGo — быстро, но нужен toolchain

5. Как в песочнице выглядит графическое приложение fyne и терминальное на bubbletea?

Проблема: настоящий fyne рисует окно через OpenGL, а браузер не даёт приложению открыть окно и не предоставляет доступ к GPU так, как ожидает десктопный OpenGL. Поэтому запустить fyne как есть в браузере нельзя.

Решение для fyne — виртуальный экран. Виджет-дерево fyne разбирается, а на странице отрисовывается HTML-модель этого дерева: кнопки, списки, поля ввода, окна. Нажатие мышью отправляется обратно в Go-код как событие, и приложение на него реагирует. Пользователь видит работающее приложение, а под капотом нет ни настоящего окна, ни OpenGL — есть разбор дерева виджетов и его отрисовка средствами браузера.

Честная оговорка: это не настоящий fyne, а его модель. Работают события и обновление виджетов, но не рендеринг fyne, не темы оформления и не то, что связано с GPU.

Для bubbletea решение другое — виртуальный терминал. Программа на bubbletea — это программа, которая рисует кадры в терминал, поэтому её вывод логично показывать в терминале: на странице появляется эмулятор терминала, в который печатается текстовый вывод программы, включая управляющие последовательности от ansi-библиотек.

Что это даёт для обучения. Программы на fyne и bubbletea, которые обычно требуют десктопа, запускаются прямо на странице урока — можно смотреть, как ведёт себя интерфейс, и править код, не покидая браузер.

Чего ожидать не стоит. Графика fyne в браузере упрощена, и задачи, завязанные на тонкую отрисовку или на прямой доступ к GPU, в песочнице не воспроизводятся. Это честное ограничение технологии, а не недоработка.

// Пример fyne-кода, который работает в песочнице:
// w := window.New()
// w.SetContent(container.NewVBox(
//     widget.NewLabel("Привет"),
//     widget.NewButton("Нажми", func() { fmt.Println("нажато") }),
// ))
// w.ShowAndRun()
//
// В браузере это отрисуется HTML-моделью виджет-дерева

6. Почему код выполняется в отдельном воркере и что это даёт?

Проблема: JavaScript выполняется в одном потоке с интерфейсом. Если пользовательский Go-код крутит бесконечный цикл прямо в нём, вкладка зависнет — кнопки перестанут нажиматься, страница перестанет скроллиться, и пользователь не сможет даже закрыть вкладку, не убив её.

Решение — Web Worker: отдельный поток браузера с собственной глобальной областью. Страница и worker общаются сообщениями: страница отправляет исходник программы, worker возвращает результат — массив строк вывода и текст ошибки.

Что это даёт практически. Первое: интерфейс не блокируется. Даже если программа считает или печатает без остановки, страница отвечает, а пользователь может нажать «Остановить». Второе: воркер можно закрыть и поднять новый — это и есть механизм, который спасает при срабатывании лимита вывода: интерпретатор нельзя прервать снаружи, зато можно закрыть воркер целиком.

Третье: изоляция. У воркера своя память и своё состояние, и ошибка в пользовательском коде не роняет страницу. Что стоит учитывать при разработке. Воркер — отдельный контекст: там нет доступа к DOM, всё общение идёт через postMessage. Между главным потоком и воркером нужно согласовать протокол: идентификатор запуска, чтобы ответы приходили в правильный запрос, и обработку падения самого воркера — если он не отвечает, главный поток должен уметь пересоздать его и повторить запуск.

Отдельная деталь: проверка готовности воркера при старте. Пока модуль wasm загружается и инициализируется, запускать программу рано — поэтому первый запрос проходит через самопроверку с таймаутом, и только после неё страница считает движок готовым.

// Протокол между страницей и воркером:
// main → worker: { id: 1, source: "package main\n..." }
// worker → main: { id: 1, ok: true, output: ["строка 1"], engine: "yaegi-wasm" }
// worker → main: { id: 1, ok: false, error: "ошибка компиляции: ...", output: [...] }

7. Как отличить ошибку компиляции от ошибки выполнения и что делать с паникой?

Проблема: пользователю нужен понятный ответ, а не технический отчёт интерпретатора. К тому же ошибки бывают двух разных видов, и лечатся они по-разному: одну надо исправлять в коде, другая — в данных или в логике.

В песочнице они разделены. Ошибка компиляции — код не прошёл проверку: неверный синтаксис, неизвестный идентификатор, несовместимые типы, неиспользуемая переменная. Ошибка выполнения — код скомпилировался, но упал при выполнении: выход за границы среза, деление на ноль, обращение к nil. В ответе это видно по разным формулировкам.

Распознавание сделано по списку признаков: если сообщение содержит характерные слова вроде «expected», «undefined», «declared and not used», «mismatched types», «cannot use», это ошибка компиляции. Такое разделение нужно, потому что в сообщении интерпретатора позиция указана в формате «строка:колонка», и она не всегда совпадает с настоящим местом ошибки.

Отдельно обрабатываются рантайм-паники. Их переводят в короткие понятные сообщения: выход за границы массива или среза — «индекс за границами», деление на ноль — «деление на ноль». Точный текст совпадает с тем, который даёт упрощённый резервный интерпретатор, — иначе уроки, где ожидается конкретная формулировка, рассыпались бы при смене движка.

Что это даёт на практике. Пользователь получает сообщение о том, что делать: «ошибка компиляции» — правьте код, «ошибка выполнения» — смотрите на данные и логику. А в уроках, где задание требует поймать панику, поведение одинаково на обоих движках.

// Примеры сообщений песочницы:
// "ошибка компиляции: expected ';', found '}'"
// "ошибка компиляции: undefined: fmt.Printn"
// "ошибка выполнения: индекс за границами"
// "ошибка выполнения: деление на ноль"
// "превышен лимит времени"
// "слишком много вывода: превышен лимит строк"

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: