Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
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" // "ошибка выполнения: индекс за границами" // "ошибка выполнения: деление на ноль" // "превышен лимит времени" // "слишком много вывода: превышен лимит строк"
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Язык Go с нуля: базовый синтаксис и конструкции — Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Fyne: курс по созданию GUI-приложений на Go — Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.
- Bubble Tea: создаём TUI-приложения на Go в терминале — Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.