Как устроена память в Go
Что проверяют
Понимаете ли вы, что размещение решает компилятор через анализ побега, и знаете ли, как это проверить.
Как отвечать
Память делится на стек и кучу. У каждой горутины свой стек, начинающийся примерно с 2 КБ и растущий копированием при нехватке. Куча общая, её обслуживает сборщик мусора.
Куда попадёт переменная, решает анализ побега на этапе компиляции. Если компилятор доказал, что значение не переживёт функцию, оно остаётся на стеке, и сборщик о нём не узнает вовсе. Если ссылка утекает наружу (вернули указатель, положили в структуру, передали в интерфейс, захватили замыканием), значение уезжает в кучу.
Посмотреть решение можно прямо: go build -gcflags='-m' печатает строки вида «escapes to heap».
Практические следствия:
- указатель не значит куча: локальный указатель, не покидающий функцию, живёт на стеке;
- интерфейс часто вызывает побег: упаковка значения в
anyпочти всегда отправляет его в кучу; - предвыделение снижает нагрузку на сборщик:
make([]T, 0, n)и переиспользование буферов черезsync.Pool; - строки и слайсы это заголовки: копирование заголовка дёшево, копируются не данные.
Аллокатор устроен по образцу tcmalloc: размерные классы, локальные кэши на каждый P, поэтому выделение мелких объектов почти не требует блокировок.
Сборщик конкурентный, с трёхцветной пометкой и барьером записи, паузы доли миллисекунды. Настраивается через GOGC и GOMEMLIMIT.
Где спотыкаются
Уверены, что всё, у чего берут адрес, попадает в кучу.
Не знают про -gcflags=-m и оптимизируют вслепую.
Считают, что в Go нет утечек: подвисшая горутина или ссылка в глобальной карте держат память вечно.
Не задают GOMEMLIMIT в контейнере и ловят убийство по памяти.