Деконструкция Go: Runtime или программа, которая запускает вашу программу. Часть 3.1
- вторник, 28 июля 2026 г. в 00:00:08
В предыдущих частях мы разобрались с тем, во что превращается Go‑код, как процессор работает с памятью и каким образом реализуются примитивные атомарные операции.
Но всё это порождает закономерный вопрос.
Если CPU умеет выполнять только машинные инструкции, а операционная система знает лишь о процессах и системных потоках, то кто вообще знает о существовании горутин?
Кто увеличивает их стеки?
Кто решает, какую из них сейчас выполнять?
Кто собирает мусор?
Ответ на всё это один — Go runtime.
Но runtime — это не какая‑то магическая программа, которая находится между нашим приложением и операционной системой. Это часть самого приложения.
Давайте разбираться!
Для начала введем определение
Runtime — это код и внутренние структуры данных, обеспечивающие выполнение тех свойств языка, которые невозможно выразить только машинными инструкциями или средствами операционной системы.
Обращу внимание на то, что Runtime — это не:
Отдельный процесс;
Сервис операционной системы;
Аналог JVM;
Интерпретатор;
Только планировщик;
Импортируемый пакет runtime.(вообще этот пакет предоставляет только публичные функции)
Проще говоря, это некий набор системных функций, которые управляют какими‑либо частями нашего кода(например, планирование операций или выделение памяти)
То есть если мы сделаем например такой код:
package main func main() { }
А потом прокатим его через
go build -o app main.go go tool nm app
То увидим нечто в духе
1400233e0 T runtime.gcAssistAlloc.func2 140023480 T runtime.gcAssistAlloc1 140020880 T runtime.gcBeginWork 140020120 T runtime.gcBgMarkStartWorkers 140020280 T runtime.gcBgMarkStartWorkers.gowrap1 1400202c0 T runtime.gcBgMarkWorker 14006cb20 T runtime.gcBgMarkWorker.func1 140020620 T runtime.gcBgMarkWorker.func2 14016818c D runtime.gcBgMarkWorkerCount 140168300 D runtime.gcBgMarkWorkerPool 140168480 D runtime.gcBitsArenas 14016814c D runtime.gcBlackenEnabled 1401685c0 D runtime.gcCPULimiter 140123100 D runtime.gcCleanups
Значения тут примерно такие:
1400233e0 — адрес символа в бинарнике(то есть либо адрес переменной, либо начало блока функции);
T/D — исполняемый код(text)/глобальная переменная;
runtime.gcAssistAlloc.func2 — название функции/переменной.
Конкретный список зависит от версии Go, архитектуры, режима линковки и оптимизаций, поэтому не могу обещать полностью одинаковый вывод.
Мы не импортировали большую часть этих функций! Их добавил toolchain, потому что без них программа не сможет выполняться по правилам Go.
И да, стандартный Go toolchain действительно включает runtime library в каждое приложение.
Неоднократно слышал, что runtime — это отдельный тред. Так вот, это неправда! Go runtime — это встроенная подсистема выполнения Go‑кода. Инструментарий.
Это разве что для нас, как для пользователей языка. На самом деле все начинается с инициализации runtime, иначе как можно выполнять без среды выполнения?
Когда мы пишем Go‑программу, её точкой входа для нас является функция:
func main() { // Стартап, который изменит мир }
Передает ли операционная система после запуска бинарника сразу передаёт управление в main.main? Нет!
Но операционка же умеет только создавать треды, перекладывать байтики, работать с адресным пространством.
И откуда тут взяться всем нашим GC и планировщикам? А всё просто:
Для обычного исполняемого файла под amd64 при внутренней линковке такой точкой входа является _rt0 amd64(сори, что без подчеркивания после rt0, тут маркдаун).
Посмотрим на исходный код runtime:
TEXT _rt0_amd64(SB),NOSPLIT,$-8 MOVQ 0(SP), DI; Из начального стека процесса извлекается количество аргументов командной строки - argc LEAQ 8(SP), SI; В регистр SI помещается адрес массива аргументов - argv. JMP runtime·rt0_go(SB) ; передаем управление Go
Да, кстати, в том числе по этой причине мы не пишем в Go int argc, byte **argv, как мы это делаем например в Си
Но что делает runtime.rt0_go?
Мы это разберем отдельно, а на данный момент остановимся на том, что он:
Создаёт начальные структуры g0 и m0;
Настраивает доступ к данным текущего системного потока;
Получает аргументы программы;
Выполняет платформенную инициализацию;
Инициализирует планировщик;
Создаёт первую обычную goroutine;
Запускает выполнение системного потока.
А если хочется посмотреть исходники, то можно поискать вот такой фрагмент:
CALL runtime·args(SB) CALL runtime·osinit(SB) CALL runtime·schedinit(SB) MOVQ $runtime·mainPC(SB), AX CALL runtime·newproc(SB) CALL runtime·mstart(SB)
Как видите, runtime.mainPC содержит ссылку на функцию runtime.main. Она передаётся в runtime.newproc, которая создаёт новую goroutine и помещает её в очередь готовых к выполнению gorутин. После этого runtime.mstart запускает начальный системный поток runtime.
runtime.mainпродолжает инициализацию среды выполнения:
Устанавливает ограничения стеков;
Разрешает создание дополнительных системных потоков;
Запускает системный монитор sysmon;
Выполняет функции инициализации самого runtime;
Включает сборщик мусора;
Выполняет init всех пакетов программы;
Вызывает пользовательскую main.main.
Итак, мы уже поняли, что runtime — это не одна функция и не один фоновый процесс.
Это набор связанных между собой подсистем, каждая из которых отвечает за определённую часть выполнения Go‑программы.
В целом runtime традиционно делят на такие сущности:
Запуск и инициализация программы;
Планировщик goroutine. Go Scheduler;
Управление стеками;
Управление памятью;
GC, garbage collector, сборщик мусора;
Netpoller;
Таймеры;
Системные вызовы и сигналы;
panic и defer;
Диагностика.

Но если что, это не строго независимые модули! В исходниках Go это представляет из себя страшное спагетти, но чисто логически часто разделяют примерно так.
Центральной сущностью здесь является планировщик. Собственно именно он и выдает права настоящим тредам ОС выполнять Go код(советую мыслить именно в этой парадигме). Как я ранее сказал, никакого отдельного треда для runtime нет. Есть только переменные и функции, которые наш дорогой CPU выполняет. Если рассматривать runtime под таким углом, то вырисовывается следующая схема выполнения:
Реальный тред через планировщик узнает, может ли он выполнить код(то есть смотрит необходимые переменные)
Получает готовую горутину
Переключается на её стек и выполняет
Опять же функции планировщика могут исполнять разные потоки и дать права на выполнение другому треду. Получаем очередное спагетти
Планировщик управляет системными потоками, но функции самого планировщика выполняются этими же системными потоками.

В следующих статьях будем уже разбираться как внутри работает каждый компонент Go Runtime, так что ждем‑с, не теряемся