На middle и senior собесах спрашивают, где Go размещает переменные, как работает GC и как найти, что тормозит или течёт.
Стек и куча
У каждой горутины свой стек, он растёт копированием (старый стек копируется в вдвое больший). Выделение на стеке почти бесплатно и не нагружает GC. Куча общая, её чистит сборщик.
Куда попадёт переменная, решает компилятор, а не new или &. Это escape analysis: если значение может пережить функцию или компилятор не может это доказать, оно «убегает» в кучу.
func newUser() *User {
u := User{Name: "a"}
return &u // u убегает в кучу: указатель живёт после возврата
}
Типичные причины побега: возврат указателя, сохранение в глобальную переменную или поле, передача в интерфейс (часто, например fmt.Println(x)), замыкание, захватывающее переменную, слайс неизвестного на этапе компиляции размера. Посмотреть: go build -gcflags=-m.
Сборщик мусора
GC в Go конкурентный, неперемещающий, трёхцветный mark-and-sweep без поколений.
- Короткая пауза stop-the-world: включается write barrier.
- Конкурентная маркировка: объекты белые (не проверены), серые (найдены, но поля не просмотрены), чёрные (просмотрены). Начинаем с корней (стеки, глобальные переменные), пока серые не кончатся.
- Write barrier не даёт программе спрятать белый объект за чёрным во время маркировки.
- Короткая пауза: завершение маркировки.
- Sweep: белые объекты освобождаются лениво, по мере новых аллокаций.
Паузы обычно меньше миллисекунды. Цена это процессорное время: горутины, которые много аллоцируют, привлекаются помогать маркировке (mark assist).
GOGC и GOMEMLIMIT
GOGC=100(по умолчанию): следующий цикл GC начнётся, когда живая куча вырастет на 100% от размера после прошлой сборки. Больше значение: реже GC, больше памяти.GOMEMLIMIT(Go 1.19+): мягкий лимит памяти. При приближении к нему GC работает чаще. В контейнере ставят около 80-90% лимита, иначе процесс может убить OOM killer раньше, чем GC проснётся.GOGC=offплюсGOMEMLIMIT: собирать только у лимита.
В Go 1.25 появился экспериментальный сборщик Green Tea (GOEXPERIMENT=greenteagc), который маркирует память блоками, лучше используя кеш процессора.
Как меньше нагружать GC
- Предвыделять слайсы и мапы:
make([]T, 0, n). - Переиспользовать буферы через
sync.Pool. - Меньше указателей в больших структурах: GC сканирует только память с указателями.
strings.Builderвместо+=.
Утечки в Go
Сборщик есть, а утечки бывают: зависшие горутины, растущие без ограничения мапы-кеши (мапа не уменьшает свою таблицу после удаления ключей), маленький срез большого массива, забытые тикеры в старых версиях, незакрытые resp.Body.
pprof
import _ "net/http/pprof" // регистрирует /debug/pprof/ на DefaultServeMux
go http.ListenAndServe("localhost:6060", nil)
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30 # CPU
go tool pprof http://localhost:6060/debug/pprof/heap # память
curl http://localhost:6060/debug/pprof/goroutine?debug=2 # стеки горутин
- profile (CPU): где тратится время, сэмплирование 100 раз в секунду.
- heap:
inuse_space(что живо сейчас, для утечек) иalloc_space(сколько всего выделено, для давления на GC). - goroutine: стеки всех горутин, ищем утечки.
- block и mutex: где ждут на синхронизации, включаются
runtime.SetBlockProfileRateиSetMutexProfileFraction. - trace (
go tool trace): временная шкала планировщика, GC, блокировок.
В интерфейсе pprof: top, list Func, web или -http=:8080 с флеймграфом. Не открывайте pprof наружу: только внутренний порт.
Из тестов: go test -bench . -cpuprofile cpu.out -memprofile mem.out.
Что спросят на собесе
- Где размещаются переменные и что такое escape analysis.
- Как работает трёхцветный GC и зачем write barrier.
- Что делают GOGC и GOMEMLIMIT, что выставить в контейнере.
- Как найти утечку памяти или горутин.
- Какие профили есть в pprof и чем inuse отличается от alloc.