Garbage Collector и Runtime в Go
Кратко: Go runtime — это слой между твоей программой и ОС. Он управляет горутинами (планировщик), памятью (allocator), и автоматически очищает мусор (GC). Понимать GC и runtime нужно, чтобы писать код, который не тормозит из-за пауз, не утекает память и не аллоцирует впустую.
Зачем знать джуну: На собеседовании спросят “что такое escape analysis?”, “как работает GC?”, “почему моя программа жрёт память?”. В production к тебе придут с GOMEMLIMIT в k8s и pprof heap-профилем — нужно понимать, что показывают цифры.
Содержание
Заголовок раздела «Содержание»- Базовое API: runtime и debug пакеты
- Под капотом: tri-color mark and sweep, write barrier, stack vs heap
- Gotchas: типичные ошибки с памятью
- Производительность: бенчмарки, sync.Pool, pprof
- Вопросы на собесе
- Practice
- Источники
1. Базовое API runtime и debug пакеты
Заголовок раздела «1. Базовое API runtime и debug пакеты»1.1 Зачем нужен GC?
Заголовок раздела «1.1 Зачем нужен GC?»В C/C++ ты сам вызываешь malloc/free. Это даёт контроль, но порождает баги:
- Memory leak — забыл
free, память течёт. - Double free — освободил дважды → undefined behavior.
- Use-after-free — обратился к освобождённой памяти → crash или security hole.
- Dangling pointer — указатель на освобождённую память.
Go решает это автоматически: ты пишешь x := &Foo{} и не думаешь про освобождение. GC сам найдёт, когда объект больше никем не используется (нет ссылок), и заберёт память.
Цена — паузы (STW, stop-the-world) и накладные расходы. Но в Go GC настолько оптимизирован, что паузы — субмиллисекундные.
1.2 runtime.GC() — ручной запуск
Заголовок раздела «1.2 runtime.GC() — ручной запуск»package main
import ( "fmt" "runtime")
func main() { // Создали мусор for i := 0; i < 1_000_000; i++ { _ = make([]byte, 1024) }
var m1, m2 runtime.MemStats runtime.ReadMemStats(&m1) fmt.Printf("До GC: HeapAlloc=%d KB\n", m1.HeapAlloc/1024)
runtime.GC() // блокирующий запуск GC
runtime.ReadMemStats(&m2) fmt.Printf("После GC: HeapAlloc=%d KB\n", m2.HeapAlloc/1024)}⚠️ В production почти никогда не нужно. GC сам решит. Ручной вызов — для тестов, бенчмарков или редких сценариев (например, после массивной загрузки).
1.3 debug.FreeOSMemory()
Заголовок раздела «1.3 debug.FreeOSMemory()»import "runtime/debug"
debug.FreeOSMemory()Принудительно запускает GC и пытается вернуть память операционной системе. Без этого Go может удерживать освобождённую память “про запас” — для будущих аллокаций.
⚠️ Зачем нужно: В Kubernetes твой под показывает RSS=2GB, хотя живые объекты — 200MB. ОС не получает память обратно. FreeOSMemory() помогает в редких случаях (long-running batch jobs).
1.4 runtime.MemStats — статистика памяти
Заголовок раздела «1.4 runtime.MemStats — статистика памяти»var m runtime.MemStatsruntime.ReadMemStats(&m)
fmt.Printf("HeapAlloc = %v MB\n", m.HeapAlloc/1024/1024) // живые объектыfmt.Printf("HeapSys = %v MB\n", m.HeapSys/1024/1024) // выделено у ОСfmt.Printf("HeapInuse = %v MB\n", m.HeapInuse/1024/1024) // используетсяfmt.Printf("HeapIdle = %v MB\n", m.HeapIdle/1024/1024) // свободно, но не отдано ОСfmt.Printf("HeapReleased = %v MB\n", m.HeapReleased/1024/1024) // отдано ОСfmt.Printf("NumGC = %v\n", m.NumGC) // сколько раз GC бежалfmt.Printf("PauseTotalNs = %v\n", m.PauseTotalNs) // суммарная паузаfmt.Printf("Mallocs = %v\n", m.Mallocs) // всего аллокацийfmt.Printf("Frees = %v\n", m.Frees) // всего освобожденийЭто первое, что смотрят при подозрении на утечку памяти.
1.5 GOGC переменная
Заголовок раздела «1.5 GOGC переменная»GOGC=100 (дефолт) — GC запустится, когда heap вырастет на 100% относительно “живого” после прошлого GC.
Примеры:
- После GC живых объектов 100MB → следующий GC при heap=200MB.
GOGC=50→ следующий GC при heap=150MB (агрессивнее, больше CPU на GC, меньше памяти).GOGC=200→ следующий GC при heap=300MB (реже, больше памяти, меньше CPU).GOGC=off→ GC отключён (только для бенчей, никогда в prod!).
debug.SetGCPercent(50) // программноGOGC=50 ./myapp1.6 GOMEMLIMIT (Go 1.19+)
Заголовок раздела «1.6 GOMEMLIMIT (Go 1.19+)»Очень важно для контейнеров (Kubernetes, Docker)!
GOMEMLIMIT=512MiB ./myappИли программно:
debug.SetMemoryLimit(512 << 20) // 512 MBЭто soft limit — Go будет агрессивнее запускать GC при приближении к лимиту. Не гарантия, что не выйдешь за предел, но снижает риск OOM-kill.
⚠️ Без GOMEMLIMIT в k8s твой под может убиться OOMKilled, потому что Go не знает про cgroup-лимиты и тянет память, пока ОС не убьёт процесс.
Best practice: в k8s ставь GOMEMLIMIT чуть ниже limits.memory пода (например, 90%).
# k8s pod specenv: - name: GOMEMLIMIT value: "450MiB"resources: limits: memory: "512Mi"2. Под капотом
Заголовок раздела «2. Под капотом»2.1 Tri-color mark and sweep — основная идея
Заголовок раздела «2.1 Tri-color mark and sweep — основная идея»Go использует concurrent tri-color mark and sweep GC. Объекты делятся на три цвета:
- Белые — кандидаты на удаление (изначально все).
- Серые — найдены, но их потомки ещё не проверены (worklist).
- Чёрные — найдены и все их потомки тоже (живые).
ASCII-схема инварианта
Заголовок раздела «ASCII-схема инварианта»Старт (после finding roots):
[ROOT]──→(A)──→(B)──→(C) │ └──→(D)──→(E)
[F] (мусор, никем не достижим)
Цвета (начало mark phase): Чёрный: ничего Серый: A (root → A напрямую) Белый: B, C, D, E, F
Шаг 1: обрабатываем A → серым становится B, A → чёрный Чёрный: A Серый: B Белый: C, D, E, F
Шаг 2: обрабатываем B → серыми становятся C, D, B → чёрный Чёрный: A, B Серый: C, D Белый: E, F
Шаг 3: обрабатываем C → C нет потомков, C → чёрный Чёрный: A, B, C Серый: D Белый: E, F
Шаг 4: обрабатываем D → E серый, D → чёрный Чёрный: A, B, C, D Серый: E Белый: F
Шаг 5: E без потомков, E → чёрный Чёрный: A, B, C, D, E Серый: ∅ Белый: F
Финал: F остался белым → удаляется в sweep phase.Инвариант tri-color:
Чёрный объект не может иметь ссылку на белый напрямую (без серого посредника).
Если это правило нарушится во время concurrent GC (когда программа работает параллельно), GC может пропустить живой объект и удалить его. Чтобы этого не случилось — write barrier.
2.2 Write barrier
Заголовок раздела «2.2 Write barrier»Когда программа делает a.field = b, и a чёрный, а b белый, write barrier “красит” b в серый (или ставит в worklist). Так инвариант сохраняется.
В Go 1.8+ используется hybrid write barrier (Dijkstra + Yuasa) — он работает быстро и снижает паузы.
// Это компилятор автоматически вставляет write barrier:obj.field = newPtr
// Эквивалентно (упрощённо):writeBarrier(obj, newPtr) // GC знает про изменениеobj.field = newPtrТебе не надо ничего делать — компилятор сам вставляет вызовы.
2.3 GC цикл: mark + sweep
Заголовок раздела «2.3 GC цикл: mark + sweep»┌─────────────────────────────────────────────────────────────┐│ GC cycle (упрощённо): ││ ││ 1. STW (sweep termination) ~10-50 µs ││ - заканчиваем sweep предыдущего цикла ││ ││ 2. Mark phase (concurrent с программой) ~ms ││ - находим roots (стек горутин, глобалы) ││ - обходим граф объектов ││ - write barrier защищает инвариант ││ - программа работает! ││ ││ 3. STW (mark termination) ~10-100 µs ││ - финализируем mark, проверяем worklist ││ ││ 4. Sweep phase (concurrent + lazy) ││ - освобождаем белые объекты ││ - lazy: при следующей аллокации ││ │└─────────────────────────────────────────────────────────────┘Concurrent — большая часть работы происходит параллельно с твоей программой. STW-паузы — только в начале и конце mark phase, обычно < 1 ms.
2.4 Stack vs Heap
Заголовок раздела «2.4 Stack vs Heap»Stack (стек)
Заголовок раздела «Stack (стек)»- Каждая горутина имеет свой стек.
- Старт: 2 KB. Растёт по необходимости до GBs.
- Не управляется GC! Очищается автоматически при возврате из функции.
- Аллокация очень дешёвая: просто двигаем указатель.
func foo() int { x := 42 // на стеке return x + 1}Heap (куча)
Заголовок раздела «Heap (куча)»- Общая для всех горутин.
- Управляется GC.
- Аллокация дороже: нужен mutex, поиск свободного блока, write barrier.
func bar() *int { x := 42 return &x // x "escapes" на heap}Здесь x не может жить на стеке bar — мы возвращаем указатель, который переживёт фрейм. Компилятор перенесёт x на heap.
2.5 Escape analysis
Заголовок раздела «2.5 Escape analysis»Компилятор анализирует, “убегает” ли значение за пределы текущей функции:
go build -gcflags="-m" main.goПример:
package main
func makePtr() *int { x := 42 return &x // ./main.go:5:9: moved to heap: x}
func notEscape() int { x := 42 return x // на стеке, всё ок}
func sliceEscape() []int { s := make([]int, 1000) // ./main.go:13:11: make([]int, 1000) escapes to heap return s}Что чаще всего escape’ит на heap?
Заголовок раздела «Что чаще всего escape’ит на heap?»- Возврат указателя/слайса/мапы из функции.
- Передача в интерфейс.
fmt.Println(x)— x может escape, так как Println принимаетinterface{}. - Захват замыканием по указателю.
- Слишком большой объект — компилятор может решить не класть на стек.
- Размер неизвестен в compile-time —
make([]int, n)гдеnruntime-переменная.
func intoInterface(x int) { fmt.Println(x) // x escape (boxing в interface{})}⚠️ Подвох: даже fmt.Println(42) делает аллокацию из-за бокса int в interface{}.
2.6 Goroutine stack growth
Заголовок раздела «2.6 Goroutine stack growth»Раньше (до Go 1.4): split stacks — стек состоял из связанных кусков. При нехватке выделялся новый, при возврате — освобождался. Это было медленно из-за “hot split” проблемы (тонкая граница между фреймами, постоянное переключение).
С Go 1.4: contiguous stacks — один непрерывный кусок. При нехватке:
- Аллоцируется новый стек в 2 раза больше.
- Содержимое копируется.
- Все указатели в стеке обновляются (это возможно, потому что GC знает, где указатели).
Старт всегда 2 KB. Может вырасти до 1 GB (по умолчанию).
debug.SetMaxStack(1 << 30) // 1 GB лимит на стек горутины3. Gotchas
Заголовок раздела «3. Gotchas»⚠️ 3.1 “Утечка” памяти через slice
Заголовок раздела «⚠️ 3.1 “Утечка” памяти через slice»big := make([]byte, 10_000_000)small := big[:10] // small держит ссылку на весь массив big!big = nil // не освободится, пока small живРешение: скопировать в новый слайс:
small := make([]byte, 10)copy(small, big[:10])big = nil // теперь освободится⚠️ 3.2 Утечка через горутины
Заголовок раздела «⚠️ 3.2 Утечка через горутины»func leak() { ch := make(chan int) // unbuffered go func() { val := compute() ch <- val // блокируется навсегда, если никто не читает }() // забыли прочитать из ch → горутина висит, держит память}Решение: context с таймаутом, buffered channel, или select с default.
⚠️ 3.3 Map не освобождает память
Заголовок раздела «⚠️ 3.3 Map не освобождает память»m := make(map[int]int)for i := 0; i < 1_000_000; i++ { m[i] = i}for k := range m { delete(m, k)}// m всё ещё держит выделенные buckets!m = nil // или make новый mapДелец только помечает слот как пустой. Внутренние buckets не уменьшаются.
⚠️ 3.4 Финализаторы — не используй для критичных ресурсов
Заголовок раздела «⚠️ 3.4 Финализаторы — не используй для критичных ресурсов»runtime.SetFinalizer(obj, func(o *MyObj) { o.Close()})Финализатор может никогда не вызваться. Не закрывай в нём файлы или соединения — используй defer Close().
⚠️ 3.5 Strings и подстроки
Заголовок раздела «⚠️ 3.5 Strings и подстроки»big := strings.Repeat("a", 1_000_000)sub := big[:10] // sub держит весь big!Аналогично слайсам. Решение: sub := string([]byte(big[:10])) или strings.Clone(big[:10]) (Go 1.18+).
⚠️ 3.6 GOMAXPROCS в контейнерах
Заголовок раздела «⚠️ 3.6 GOMAXPROCS в контейнерах»Без automaxprocs или Go 1.25+ runtime может видеть все CPU хоста, а не лимит cgroup. Это приводит к чрезмерной параллельности и контеншну.
import _ "go.uber.org/automaxprocs"⚠️ 3.7 sync.Pool — pool может очиститься в любой момент
Заголовок раздела «⚠️ 3.7 sync.Pool — pool может очиститься в любой момент»var pool = sync.Pool{ New: func() any { return new(MyObj) },}
obj := pool.Get().(*MyObj)// ... use ...pool.Put(obj)Pool не гарантирует, что объект ты получишь обратно. После каждого GC pool очищается (с Go 1.13 — частично, “victim cache” живёт один GC цикл). Не клади туда то, что должно жить долго.
⚠️ 3.8 Эскейп в loop closures
Заголовок раздела «⚠️ 3.8 Эскейп в loop closures»for i := 0; i < 10; i++ { go func() { fmt.Println(i) // Go 1.22+: i свой; раньше — общая переменная! }()}В Go 1.22 семантика переменных цикла изменена: каждая итерация имеет свою i. До 1.22 — общая, что приводило к классическому багу.
⚠️ 3.9 GC паузы под нагрузкой
Заголовок раздела «⚠️ 3.9 GC паузы под нагрузкой»Если ты создаёшь миллионы коротко-живущих объектов в секунду, GC работает почаще. CPU съедается GC. Решение: sync.Pool, переиспользование буферов, меньше указателей.
⚠️ 3.10 RSS не падает
Заголовок раздела «⚠️ 3.10 RSS не падает»После пика памяти RSS контейнера может не уменьшиться, хотя HeapAlloc упал. Go удерживает память “про запас”. В k8s это пугает мониторинг. Помогает GOMEMLIMIT + периодический debug.FreeOSMemory() (но осторожно — стоит CPU).
4. Производительность
Заголовок раздела «4. Производительность»4.1 Бенчмарки с -benchmem
Заголовок раздела «4.1 Бенчмарки с -benchmem»package main
import "testing"
func BenchmarkAlloc(b *testing.B) { for i := 0; i < b.N; i++ { s := make([]byte, 1024) _ = s }}
func BenchmarkNoAlloc(b *testing.B) { buf := make([]byte, 1024) b.ResetTimer() for i := 0; i < b.N; i++ { _ = buf }}go test -bench=. -benchmemВывод:
BenchmarkAlloc-8 3000000 400 ns/op 1024 B/op 1 allocs/opBenchmarkNoAlloc-8 1000000000 0.3 ns/op 0 B/op 0 allocs/opB/op— байт на операцию.allocs/op— аллокаций на операцию.
Цель: уменьшить allocs/op до 0 в горячем пути.
4.2 sync.Pool — переиспользование объектов
Заголовок раздела «4.2 sync.Pool — переиспользование объектов»import "sync"
var bufPool = sync.Pool{ New: func() any { return make([]byte, 0, 1024) },}
func processRequest(data []byte) { buf := bufPool.Get().([]byte) buf = buf[:0] // сброс длины, capacity сохранён defer bufPool.Put(buf)
// ... work with buf ... buf = append(buf, data...) // ...}Use case: хендлеры HTTP, JSON-encoding, парсеры. Тысячи аллокаций → десятки.
⚠️ Гочи:
- Не клади указатели, которые могут escape’ить дальше.
- Pool может вернуть объект в любом состоянии — не забывай сбрасывать (
buf[:0]). - Не подходит для больших объектов с длинной жизнью.
4.3 pprof — heap profile
Заголовок раздела «4.3 pprof — heap profile»import _ "net/http/pprof"import "net/http"
func main() { go func() { http.ListenAndServe("localhost:6060", nil) }() // ... твоя программа ...}# Снять heap profilego tool pprof http://localhost:6060/debug/pprof/heap
# В интерактиве:(pprof) top10(pprof) list MyFunc(pprof) web # граф в браузереИли прямо в бенчмарке:
go test -bench=. -memprofile=mem.outgo tool pprof mem.outВиды heap-профилей
Заголовок раздела «Виды heap-профилей»inuse_space(дефолт) — память живых объектов.inuse_objects— кол-во живых объектов.alloc_space— всего выделено за время работы.alloc_objects— всего объектов выделено.
alloc_* хорош для понимания “горячих” аллокаций, inuse_* — для утечек.
4.4 Тюнинг GOGC и GOMEMLIMIT
Заголовок раздела «4.4 Тюнинг GOGC и GOMEMLIMIT»Сценарии:
| Сценарий | Рекомендация |
|---|---|
| Latency-критичный API | GOGC=100, GOMEMLIMIT=80% от limits |
| Batch обработка | GOGC=200-500 (меньше GC, больше памяти) |
| Жёсткий лимит памяти в k8s | GOMEMLIMIT + GOGC=100 |
| Low memory edge device | GOGC=50, GOMEMLIMIT=... |
Для джуна важно понимать смысл переменных, не запоминать конкретные значения.
4.5 Уменьшаем аллокации — практика
Заголовок раздела «4.5 Уменьшаем аллокации — практика»- Preallocate slices:
make([]int, 0, n)вместоvar s []int. - string builder вместо +:
strings.Builderдля конкатенации. bytes.Buffer/bufio.Writerдля I/O.- Не передавай interface{} в hot path.
- Reuse через sync.Pool для коротко-живущих больших объектов.
- Структуры по значению vs указателю — иногда значение лучше (меньше указателей → меньше работы GC).
- strings.Builder vs []byte — Builder использует unsafe чтобы избежать копии.
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»Базовые
Заголовок раздела «Базовые»1. Что такое GC и зачем он нужен? Garbage Collector — автоматическое управление памятью. Освобождает объекты, на которые нет ссылок. Избавляет от багов ручного управления (leak, use-after-free), но добавляет паузы и оверхед.
2. Какой алгоритм GC в Go? Concurrent tri-color mark and sweep с hybrid write barrier (Dijkstra + Yuasa). Concurrent — значит большая часть работы параллельно с программой.
3. Что такое STW (stop-the-world)? Пауза, когда GC останавливает все горутины. В Go только короткие STW в начале и конце mark phase — обычно <1 ms.
4. Расскажи про tri-color. Объекты — белые (мусор), серые (найдены, потомки не проверены), чёрные (живые). Mark phase обходит граф, перекрашивая. Белые в конце — удаляются.
5. Зачем write barrier? Чтобы инвариант “чёрный → белый напрямую” не нарушался при concurrent работе программы. Когда программа меняет ссылку, write barrier сообщает GC.
6. Stack vs heap — где аллоцируется? Решает компилятор через escape analysis. Если значение не “убегает” за пределы функции — на стек (дёшево). Если убегает — на heap (управляется GC).
7. Что такое escape analysis?
Compile-time анализ: куда положить переменную — стек или heap. Запустить: go build -gcflags="-m".
8. Что такое GOGC?
Переменная, контролирующая агрессивность GC. GOGC=100 (дефолт) — следующий GC при удвоении heap. GOGC=off — отключить (только для тестов).
9. Что такое GOMEMLIMIT? Soft memory limit (Go 1.19+). Go будет агрессивнее запускать GC, приближаясь к лимиту. Критично в Kubernetes — без него возможен OOMKilled.
10. Какой стартовый размер стека горутины? 2 KB. Растёт до 1 GB (по умолчанию) через копирование (contiguous stacks с Go 1.4+).
Средние
Заголовок раздела «Средние»11. Что такое split stacks vs contiguous stacks? Split (до Go 1.4) — связанный список кусков стека, страдал от hot-split (медленно при пересечении границы). Contiguous (Go 1.4+) — один непрерывный кусок, при нехватке аллоцируется новый в 2x и копируется содержимое.
12. Почему fmt.Println(42) делает аллокацию?
Println принимает interface{}. int упаковывается (boxing) в interface, что требует heap-аллокации.
13. Утечка памяти в Go возможна? Не в смысле C (забыл free). Но возможна через:
- висящие горутины,
- глобальные мапы/слайсы, куда добавляют и не удаляют,
- ссылки на большой массив через slice[:10],
- финализаторы, которые не запустились.
14. Что делает runtime.GC()? Принудительно запускает GC, блокируется до завершения. Почти не нужен в проде.
15. Что такое sync.Pool? Пул объектов для переиспользования. Уменьшает аллокации. Объекты могут быть удалены в любой момент (GC очищает pool). Не для критичных ресурсов!
16. Когда sync.Pool НЕ помогает?
- Объекты долго живут (pool бесполезен).
- Объекты слишком маленькие (overhead Pool > выгода).
- Объекты разных размеров — Pool не различает.
17. Что такое hybrid write barrier? Комбинация Dijkstra (защита от чёрный → белый) и Yuasa (защита удалением ссылок). Позволяет не сканировать стеки повторно, уменьшая STW.
18. Как посмотреть escape analysis?
go build -gcflags="-m" main.go. Покажет, что moved to heap.
Продвинутые
Заголовок раздела «Продвинутые»19. Что показывает HeapAlloc vs HeapSys? HeapAlloc — живые объекты (то, что не освобождено). HeapSys — сколько Go запросил у ОС. HeapSys всегда >= HeapAlloc.
20. Почему RSS контейнера не падает после GC?
Go удерживает память “про запас” для будущих аллокаций. debug.FreeOSMemory() или GOMEMLIMIT помогают вернуть память ОС.
21. Сколько примерно GC-пауз в типичном API? Обычно <1 ms. В тяжёлых сценариях (миллионы объектов) — единицы ms. STW редко превышает 10 ms.
22. Что произойдёт, если включить GOGC=off в production? GC не будет запускаться никогда → память будет расти бесконечно → OOM.
23. Расскажи про утечку через map.
delete(m, k) помечает слот пустым, но buckets не уменьшаются. Чтобы освободить — m = nil или m = make(map[K]V).
24. Как уменьшить аллокации?
make([]T, 0, n)— preallocate capacity.sync.Poolдля reuse.strings.Builderвместо+.- Избегать
interface{}в hot path. - Передача по значению, если структура маленькая.
25. Можно ли отключить GC для performance?
Можно (GOGC=off), но это диверсия. Лучше: уменьшить аллокации, использовать sync.Pool, тюнить GOGC.
6. Practice
Заголовок раздела «6. Practice»Задача 1: Найди утечку
Заголовок раздела «Задача 1: Найди утечку»package main
import "time"
type Server struct { handlers map[string]func()}
func (s *Server) AddHandler(name string, fn func()) { s.handlers[name] = fn}
func main() { s := &Server{handlers: make(map[string]func())}
for i := 0; i < 1_000_000; i++ { bigData := make([]byte, 10_000) s.AddHandler("h", func() { _ = bigData // замыкание держит bigData! }) }
time.Sleep(10 * time.Second)}Что не так? Каждая итерация переписывает handler “h”, но 999999 замыканий с bigData удерживают данные через старые ссылки? Нет, в map один ключ. Но bigData каждой итерации сохраняется, пока handler “h” не перезапишется. Глянь внимательнее.
На самом деле в этом коде — map содержит один handler в конце, но в процессе цикла все bigData могут escape’ить на heap. Реальная проблема — что замыкания тяжёлые. Перепиши, чтобы не было утечки больших данных.
Задача 2: Бенчмарк аллокаций
Заголовок раздела «Задача 2: Бенчмарк аллокаций»Напиши два варианта функции, конкатенирующей слайс строк, и сравни:
// Вариант 1: через +=func ConcatPlus(parts []string) string { s := "" for _, p := range parts { s += p } return s}
// Вариант 2: через strings.Builderfunc ConcatBuilder(parts []string) string { var b strings.Builder for _, p := range parts { b.WriteString(p) } return b.String()}Запусти бенчмарк, посмотри B/op и allocs/op. Объясни разницу.
Задача 3: sync.Pool в HTTP-хендлере
Заголовок раздела «Задача 3: sync.Pool в HTTP-хендлере»Напиши HTTP-хендлер, который читает JSON из body и эхует обратно. Сравни две версии:
- Без pool — каждый раз
make([]byte, 4096). - С
sync.Poolбуферов.
Прогони wrk или ab, посмотри разницу в allocs/req через pprof.
Задача 4: pprof heap
Заголовок раздела «Задача 4: pprof heap»Запусти простой сервер с net/http/pprof. Создавай нагрузку. Сними heap-профиль:
go tool pprof http://localhost:6060/debug/pprof/heapНайди топ-3 функции по inuse_space.
Задача 5: Escape analysis
Заголовок раздела «Задача 5: Escape analysis»Возьми любой свой код и запусти go build -gcflags="-m". Найди 3 строки, где значение moved to heap, и попробуй переписать, чтобы оно осталось на стеке.
7. Источники
Заголовок раздела «7. Источники»- Go Memory Management — Go Blog: Getting to Go — keynote Rick Hudson про GC.
- A Guide to the Go Garbage Collector — официальная документация: https://go.dev/doc/gc-guide
- Достаточно ли вы знаете про GOMEMLIMIT — https://weaviate.io/blog/gomemlimit-a-game-changer-for-high-memory-applications
- pprof tutorial — https://github.com/google/pprof/blob/main/doc/README.md
- Effective Go — https://go.dev/doc/effective_go (раздел про concurrency и память)
- Книга: Cox-Buday — Concurrency in Go (главы про runtime).
- Книга: 100 Go Mistakes and How to Avoid Them — Teiva Harsanyi (главы 12-13 про runtime и memory).
Чек-лист джуна:
- Понимаю разницу stack vs heap.
- Знаю, как посмотреть escape analysis.
- Использую
-benchmemв бенчмарках. - Знаю про GOGC и GOMEMLIMIT.
- Умею снять heap-профиль через pprof.
- Знаю про утечки слайсов/строк через подстроки.
- Понимаю, что sync.Pool не гарантирует возврат объекта.
- Знаю про concurrent tri-color mark and sweep на уровне идеи.