30 вопросов
Планировщик (scheduler) горутин и потоков, сборщик мусора (GC), управление памятью (аллокации, куча), паника/recover, каналы и примитивы sync, сетевой поллер, системные вызовы. Код в runtime и runtime/internal; часть логики в ассемблере под платформу.
Concurrent mark-and-sweep с трицветной разметкой. Фазы: mark (разметка достижимых объектов, параллельно с приложением), mark termination (короткая пауза), sweep (подчистка неиспользуемой памяти, может быть фоновой). С Go 1.5 GC конкурентный.
Stop The World - пауза, когда все горутины останавливаются. В Go STW короткие: в основном на mark termination и при подготовке к sweep. Цель - минимизировать STW; большая часть работы GC идёт параллельно с приложением.
GOGC (по умолчанию 100): целевой процент роста кучи после GC. 100 значит "запускать GC, когда живая куча выросла в 2 раза с прошлого цикла". GOMEMLIMIT (Go 1.19+) - мягкий лимит памяти процесса; GC будет работать агрессивнее при приближении к лимиту.
GOGC=50 GOMEMLIMIT=512MiB ./appЦелевой коэффициент роста кучи 100%: следующий цикл GC запускается, когда размер живой кучи с прошлого GC вырос в 2 раза. Меньше GOGC - чаще GC, меньше пиковое потребление, больше CPU на GC. Больше GOGC - реже GC, больше памяти.
Компилятор решает по escape analysis. Если указатель на объект уходит за пределы функции (return, глобальная переменная, замыкание) - объект в куче. Локальные переменные, на которые нет ссылок снаружи - на стеке. Стек горутины ограничен; большие объекты часто в куче.
Анализ компилятора: может ли указатель на переменную "убежать" из функции. Если да - переменная размещается в куче. Запуск с -gcflags="-m" выводит решения по escape. Цель - держать данные на стеке и снижать нагрузку на GC.
go build -gcflags="-m" 2>&1 | grep escapeНачальный стек маленький (порядка 2 КБ). При нехватке места рантайм выделяет новый блок стека (больше предыдущего), копирует туда текущий стек и переключается на него. Рост ограничен лимитом (порядка гигабайта). Копирование при росте - причина отказа от сегментированного стека в старых версиях.
Сегментированный стек при переполнении выделял новый сегмент; при возврате из глубокой цепочки сегмент освобождался. Частое выделение/освобождение в горячем цикле (hot split) создавало нагрузку и фрагментацию. Перешли на непрерывный стек с копированием при росте.
Максимальное число потоков ОС (M), которые одновременно выполняют пользовательский код. По умолчанию равно числу CPU. Меняет только количество активных M; горутин может быть больше. Обычно не трогают; для отладки или ограничения параллелизма иногда уменьшают.
runtime.GOMAXPROCS(4)Импорт _ "net/http/pprof" и запуск HTTP-сервера регистрируют эндпоинты /debug/pprof/. Профили: cpu, heap, goroutine, block, mutex. Сбор через go tool pprof или через API. В production обычно включают отдельный порт или путь с ограничением доступа.
import _ "net/http/pprof"
go func() { log.Println(http.ListenAndServe(":6060", nil)) }()CPU profiling - семплирование по таймеру, накладные расходы небольшие. Heap profiling - при аллокациях, заметнее при высокой частоте. Блокировки и мьютексы - при событиях блокировки. Для production обычно включают только нужные профили и ограничивают частоту или путь.
Сравнивать heap-профили за разные моменты (pprof, go tool pprof -base). Смотреть, какие объекты накапливаются. Проверять, что горутины и каналы не остаются в ожидании навсегда. Использовать тесты с ограничением памяти и race detector. Отключать таймеры и не закрывать каналы - частые причины.
Глобальные кеши и мапы без ограничения размера. Горутины, блокирующиеся на канале без выхода. Таймеры/тикеры без Stop(). Незакрытые соединения и дескрипторы. Циклические ссылки при наличии глобальных ссылок. Удержание больших срезов в подмножестве слайса (держать ссылку на базовый массив).
Да. Go GC не подсчитывает ссылки; он помечает достижимые от корней объекты. Недостижимые циклы (никто снаружи на них не ссылается) будут собраны. Счётчики ссылок не используются.
Частично. runtime.GC() запускает полный цикл GC. Для отладки или тестов - допустимо. В production обычно не вызывают - рантайм сам выбирает момент. Для предсказуемых пауз (например, перед критичным участком) иногда используют.
runtime.GC()Избегать escape на кучу (не возвращать указатели на локальные, не захватывать в замыкание по указателю). Переиспользовать срезы (буферы) через sync.Pool или повторное использование. Избегать боксинга (interface{}, срезы из строк). Профилировать и смотреть -gcflags="-m".
Техника: в начале программы выделить большой кусок памяти (например, большой срез), чтобы куча сразу выросла. GC реже срабатывает в начале; первый цикл откладывается. Уменьшает количество ранних GC и может стабилизировать латентность. Используют осторожно.
var ballast = make([]byte, 100<<20) // 100 MBGo 1.19+: задаёт мягкий лимит памяти процесса. Рантайм старается удерживать использование памяти в рамках этого лимита, усиливая работу GC. Не жёсткий лимит - при пиках можно превысить. Помогает в контейнерах избежать OOM при сохранении производительности.
GOMEMLIMIT=512MiBProfile-Guided Optimization (Go 1.20+): компилятор использует профиль выполнения (обычно CPU) для инлайнинга и размещения кода. Сбор профиля, затем сборка с -pgo=default.pgo. Может дать прирост на несколько процентов для горячих путей.
go build -pgo=default.pgoПодстановка тела функции в место вызова вместо вызова. Уменьшает накладные расходы и даёт оптимизации across call. Компилятор решает по размеру и сложности функции. Можно смотреть через -gcflags="-m". Инлайн ограничен эвристиками; большие функции не инлайнятся.
runtime/debug.Stack() возвращает байты со стеком текущей горутины. runtime.Stack(buf, true) - в буфер, второй параметр - включить стеки всех горутин. В обработчике паники через recover можно логировать debug.Stack(). Пакет pprof выводит стеки по профилю.
fmt.Println(string(debug.Stack()))При -race компилятор добавляет инструментацию к доступам к памяти. Рантайм отслеживает пары событий и отношения happens-before. При обнаружении конфликтующего доступа (data race) выводит отчёт. Дорого по CPU и памяти - только для тестов и отладки.
Функция, вызываемая сборщиком мусора после того, как объект стал недостижим. Не гарантируется момент вызова и порядок. Используют для освобождения внешних ресурсов (C, файлы), но предпочтительнее явный Close. Нельзя полагаться на finalizer для критичной логики.
runtime.SetFinalizer(obj, func(o *T) { o.close() })Механизм в GC: при записи указателя в объект барьер помечает или обрабатывает целевую область, чтобы не потерять ссылки во время concurrent mark. Нужен для корректности трицветной разметки при работе приложения параллельно с GC.
То же, что GOMEMLIMIT: мягкий лимит памяти процесса. GC старается не превышать лимит, но не гарантирует жёсткую границу. В документации называется "soft limit".
С Go 1.5 GC по большей части конкурентный: разметка и подчистка идут параллельно с выполнением приложения. Короткие паузы только в mark termination. Write barrier позволяет приложению работать во время mark.
Регулировка темпа работы GC: когда запускать следующий цикл и как интенсивно работать, чтобы уложиться в целевое использование памяти (GOGC) и не создавать длинных пауз. Рантайм балансирует между временем GC и объёмом кучи.
В stderr выводятся строки по каждому циклу GC: фаза (mark, sweep), длительность, размер кучи до/после, время паузы и т.п. Удобно для быстрой оценки поведения GC и пауз. В production обычно отключают из-за объёма вывода.
GODEBUG=gctrace=1 ./appКомпилятор: go build -gcflags="-m" выводит решения по escape (и инлайну). Две буквы m дают больше вывода: -gcflags="-m -m". В коде нельзя "запросить" escape; только смотреть вывод компилятора и менять код, чтобы объекты не уходили в кучу.
go build -gcflags="-m" ./...