Языки и рантайм: где Go и как живёт программа
Этот урок ставит фундамент перед всем остальным фундаментом. Тут не будет кода, который ты будешь писать сам, тут будет интуиция: что вообще такое “скомпилированный язык”, куда попадает Go в этой классификации, что лежит в памяти процесса, пока твоя программа работает, и где эта программа обычно живёт в индустрии. Если эти вещи в голове, всё дальнейшее (память, указатели, интерфейсы) ляжет в правильные ячейки.
После урока зайди в 02-memory-and-parameters,
там карта памяти превратится в практику с &, * и реальными адресами.
1. Типы языков по способу исполнения
Программа на любом языке проходит путь от текста до выполнения. Способов этот путь устроить ровно три, и от выбранного способа зависит почти всё: скорость старта, размер бинаря, тип ошибок (которые ловишь на компиляции или в проде), профиль развёртывания.
| Тип | Что делает | Примеры | Старт | Размер артефакта |
|---|---|---|---|---|
| Компилируемый AOT | Транслирует исходник в машинный код до запуска. Запускаешь готовый бинарь. | C, C++, Rust, Go, Swift | мгновенный | один файл |
| Интерпретируемый | Читает исходник построчно во время выполнения, прямо так его и исполняет (или компилирует в байткод “на лету”). | Python (CPython), Ruby, классический JS, Bash | медленный | исходники плюс рантайм |
| Гибридный (JIT/VM) | Компилирует исходник в байткод, байткод исполняется виртуальной машиной, горячие участки JIT-компилируются в машинный код прямо в процессе. | Java (JVM), C# (.NET), современный JS (V8), Erlang BEAM | средний (плюс прогрев) | байткод плюс VM |
AOT это не “лучше” и не “хуже”. У каждого режима свои сделки: AOT даёт быстрый старт и предсказуемую скорость, но теряет рантайм-оптимизации, которые JIT может включать на горячих циклах. Интерпретатор медленный, но он умеет править код в работающей программе (“hot reload”), не останавливая её.
Куда попадает Go
Go это компилируемый AOT-язык со встроенным runtime. Это не похоже ни на чистый C, ни на Java:
go buildпревращает исходник в машинный код для целевой платформы. Результат это самодостаточный бинарь: ОС, libc нужного ABI, и всё, никаких “виртуальных машин” не запускается.- Внутрь бинаря зашит Go runtime. Это маленькая библиотека (~1 MB
на пустой
hello world), в которой живут планировщик горутин, garbage collector, поддержка каналов и map-ов. Поэтому даже однострочный Go-бинарь весит несколько мегабайт, в отличие от 100-килобайтного C. - Никакого JIT. Go не оптимизирует код на ходу. То что компилятор сгенерировал, то и работает. Это проще, дешевле и предсказуемее, в обмен на потолок производительности на 10-20% ниже чем у JVM на долго работающих CPU-bound сервисах.
Эту сделку команда Go выбрала сознательно: они приоритизировали время старта, размер бинаря и читаемость профиля памяти в обмен на пиковую перформансную математику. Для серверных сервисов и CLI это почти всегда правильный размен.
2. Гибридные языки и почему Go не такой
Гибридные языки часто называют “лучшими из двух миров”. Идея в том, что:
- Компилируешь в байткод, более компактный чем исходник, но более высокоуровневый чем машинный код.
- Запускаешь VM, она исполняет байткод.
- JIT в VM наблюдает за горячими участками и на лету перекомпилирует их в машинный код.
Это объясняет, почему долго работающий Java-сервис может выйти на скорость близкую к C: после прогрева hot loops уже скомпилированы.
Python: .py ──cpython─-> .pyc ─-> интерпретатор виртуальной машины ─-> результат
Java: .java ──javac──-> .class ─-> JVM (байткод-интерпретатор + JIT C1/C2) ─-> результат
JavaScript: .js ──V8──-> Ignition байткод ──TurboFan JIT──-> машинный код ─-> результат
Go: .go ──gc compiler──-> машинный код плюс встроенный runtime
Чего у Go нет, и почему это нормально:
- Нет VM, бинарь запускается напрямую ядром ОС.
- Нет JIT, оптимизация целиком в
go build. - Нет интерпретатора, нет
go run --hot-reload. Изменил исходник, запустилgo buildснова. Это занимает миллисекунды на маленьких модулях (часть 1.18+ кэширует пакеты).
Главное практическое следствие: Go-программу нельзя “запустить как
скрипт”. Если хочется быстрых итераций как в Python, ставят air
или reflex, они смотрят за файлами и пересобирают бинарь, эффект,
как у hot reload, на самом деле, обычная пересборка.
3. Что лежит в памяти запущенной программы
Когда ОС запустила твой Go-бинарь, в адресное пространство процесса попало несколько типов данных. Эта картинка одинаковая для C, Rust и Go (отличается только то, что Go положит сверху goroutine-стеки и heap-метаданные GC).
адрес ╭───────────────────────────────╮ низ
│ text инструкции (read-only)│
├───────────────────────────────┤
│ data инициализированные │
│ глобальные │
├───────────────────────────────┤
│ bss неинициализированные │
│ глобальные (zero) │
├───────────────────────────────┤
│ heap ↓ растёт вниз │
│ (динамическая память) │
│ ... │
│ ↑ растёт вверх │
│ stack стек главного потока │
├───────────────────────────────┤
│ runtime Go scheduler, GC, │
│ goroutine-стеки │
╰───────────────────────────────╯ верх
| Сегмент | Что туда попадает | Можно ли менять |
|---|---|---|
| text | скомпилированные машинные инструкции твоей программы и stdlib | нет, read-only |
| data | глобальные переменные с явным значением: var debug = true | да |
| bss | глобальные без явного значения: var counter int (получит 0) | да |
| heap | объекты, которые “пережили” фрейм функции, slice/map с make, всё, на что есть указатель из других мест | да |
| stack | локальные переменные, фреймы вызовов функций | да, через push/pop |
В Go к этой картинке добавляется ещё одна важная вещь: у каждой
горутины свой стек, который растёт динамически. У главного
потока (горутины main) он начинает с 8 KB, но рантайм может его
расширить до сотен мегабайт, если функция вошла в глубокую рекурсию.
Раньше Go-горутины росли сегментами, с 1.4 это
contiguous-stack
с копированием. Подробности в 17-concurrency-deep.
Стек vs heap, та самая граница
Главное практическое отличие, которое будет важно весь курс:
- Стек, “переменная живёт пока живёт функция”. Аллокация это просто сдвиг указателя, освобождение тоже. Без участия GC.
- Heap, “переменная переживёт функцию”. Аллокацию делает рантайм
через
mallocgc, освобождение делает garbage collector. Дороже в обе стороны.
Куда попадёт переменная, решает не программист, а escape analysis компилятора Go. Деталь разберём в 02-memory-and-parameters и углубим в 18-runtime-and-perf.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.