Zero-Allocation Patterns в Go
Зачем знать: Каждая аллокация — это работа для garbage collector. На hot path с высоким QPS аллокации становятся доминирующим фактором latency: GC pauses, cache misses, contention на runtime allocator. Zero-allocation patterns — это набор техник для написания кода, который не давит на GC. Знание escape analysis, sync.Pool, strings.Builder, unsafe-конверсий и struct alignment отличает инженера, способного писать low-latency сервисы (HFT, ad bidding, real-time gaming), от инженера, который только “пишет на Go”.
Содержание
Заголовок раздела «Содержание»- Базовая концепция (повторение)
- Production-практики
- Gotchas (10+)
- Реальные кейсы
- Вопросы (25)
- Practice (5-8)
- Источники
1. Базовая концепция (повторение)
Заголовок раздела «1. Базовая концепция (повторение)»Зачем zero-alloc
Заголовок раздела «Зачем zero-alloc»В Go каждая heap-аллокация:
- Требует работы аллокатора (mcache, mcentral, mheap)
- Увеличивает working set памяти
- Создаёт работу для GC (mark phase scans pointers)
- Может вызвать GC раньше (heap goal triggered)
- Может попасть в STW pause (~100μs на современных версиях Go)
При высоком QPS (10K+ rps) даже 5 аллокаций на запрос = 50K alloc/sec = существенный GC pressure.
Когда оправдано
Заголовок раздела «Когда оправдано»✅ Hot paths в high-throughput сервисах:
- HTTP/gRPC request handlers
- Парсеры протоколов (JSON, Protobuf, binary)
- Сериализаторы
- Hot loops в batch processing
✅ Low-latency требования:
- Financial / HFT (microsecond-level)
- Real-time gaming
- Ad bidding (real-time bidding, <100ms total)
- Database engines
✅ High-throughput servers:
- Load balancers (Cloudflare, Caddy)
- API gateways
- Message brokers
Когда НЕ оправдано
Заголовок раздела «Когда НЕ оправдано»❌ Cold paths: rarely-called функции (init, config loading) ❌ Premature optimization без бенчмарков ❌ Code clarity vs performance — если zero-alloc делает код unmaintainable, лучше profiler-driven optimization потом ❌ Когда работа dominated I/O — оптимизация аллокаций на сетевом коде не даст эффекта если узкое место — это disk/network
Правило Knuth, актуальное в 2026: “Premature optimization is the root of all evil.” Сначала меряем, потом оптимизируем.
Базовая модель аллокаций
Заголовок раздела «Базовая модель аллокаций»Go компилятор делает escape analysis — определяет, может ли переменная “сбежать” из стека (escape to heap).
func stack() { x := 42 // на стеке _ = x}
func heap() *int { x := 42 // на heap (escapes — возвращаем pointer) return &x}Проверить через флаг компилятора:
go build -gcflags="-m" main.goOutput:
./main.go:5:6: x does not escape./main.go:9:6: moved to heap: x2. Production-практики
Заголовок раздела «2. Production-практики»2.1. Tools для поиска аллокаций
Заголовок раздела «2.1. Tools для поиска аллокаций»Benchmark с -benchmem
Заголовок раздела «Benchmark с -benchmem»func BenchmarkParse(b *testing.B) { data := []byte(`{"foo":"bar"}`) b.ResetTimer() for i := 0; i < b.N; i++ { var v map[string]string json.Unmarshal(data, &v) }}go test -bench=. -benchmemBenchmarkParse-8 500000 3200 ns/op 320 B/op 8 allocs/opB/op = байт на операцию. allocs/op = количество аллокаций.
⚠️ Цель: 0 allocs/op для hot path.
pprof alloc_space
Заголовок раздела «pprof alloc_space»go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap(pprof) topПоказывает функции, которые в сумме аллоцировали больше всего памяти.
(pprof) list myHotFuncПоказывает строки с allocations.
Escape analysis
Заголовок раздела «Escape analysis»go build -gcflags="-m" ./...go build -gcflags="-m=2" ./... # более verboseOutput:
./parser.go:42:13: ([]byte)(s) escapes to heap./parser.go:45:15: result escapes to heap./parser.go:50:6: &buf escapes to heapBounds check elimination
Заголовок раздела «Bounds check elimination»Go runtime проверяет slice[i] и slice[i:j] на out-of-bounds. Иногда эти проверки можно убрать:
go build -gcflags="-d=ssa/check_bce/debug=1" ./...Output показывает строки, где BCE сработал и где нет:
./code.go:10:5: Found IsInBounds./code.go:15:8: Found IsSliceInBoundsЕсли на критической line BCE не сработал — можно reorganize код.
2.2. Allocation reduction techniques
Заголовок раздела «2.2. Allocation reduction techniques»Preallocate slices/maps
Заголовок раздела «Preallocate slices/maps»❌ Плохо:
var result []Itemfor _, x := range input { result = append(result, process(x)) // growing slice — re-allocations}✅ Хорошо:
result := make([]Item, 0, len(input))for _, x := range input { result = append(result, process(x))}Аналогично для maps:
m := make(map[string]int, expectedSize)Эффект: избегает re-allocation при росте. На больших slice/map — в разы меньше аллокаций.
sync.Pool для temp buffers
Заголовок раздела «sync.Pool для temp buffers»var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) },}
func process(input string) string { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufPool.Put(buf) }() buf.WriteString("prefix:") buf.WriteString(input) return buf.String()}Когда использовать sync.Pool:
- Объекты создаются часто
- Используются короткое время
- Размер объектов значимый (>1KB)
- Можно reset перед reuse
⚠️ Gotcha: sync.Pool может потерять объекты на GC. Это by design — не использовать как cache.
strings.Builder вместо +
Заголовок раздела «strings.Builder вместо +»❌ Плохо:
result := ""for _, s := range parts { result += s // каждое + аллоцирует новую строку}✅ Хорошо:
var b strings.Builderb.Grow(estimatedSize) // pre-allocatefor _, s := range parts { b.WriteString(s)}result := b.String()io.Copy вместо ReadAll
Заголовок раздела «io.Copy вместо ReadAll»❌ Плохо:
data, _ := io.ReadAll(r) // аллоцирует весь bufferfmt.Println(string(data))✅ Хорошо (при streaming):
io.Copy(os.Stdout, r) // без буферизации в памятиunsafe.String / unsafe.Slice (Go 1.20+)
Заголовок раздела «unsafe.String / unsafe.Slice (Go 1.20+)»Конверсия []byte ↔ string обычно копирует данные (потому что строки immutable).
// Standard — копируетs := string(byteSlice)b := []byte(s)С Go 1.20+ можно делать zero-copy через unsafe:
import "unsafe"
// []byte → string (без копии)func b2s(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b))}
// string → []byte (без копии)func s2b(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s))}⚠️ CRITICAL gotcha: результирующая string/slice разделяет память с исходными данными.
- Изменение
[]byteпослеb2s→ undefined behavior (strings должны быть immutable) - Изменение результата
s2b→ undefined behavior (память string’ов в read-only segment может быть)
Использовать только когда уверены в lifetime данных.
Avoid interface boxing
Заголовок раздела «Avoid interface boxing»В Go interface значение всегда обёрнуто в (type, data) пару. Передача scalar через interface → heap allocation.
❌ Плохо:
func log(values ...any) { // any = interface{} // ...}log(42) // 42 → boxed in interface → heap allocation✅ Лучше (если known type):
func logInt(value int) { // ...}Или type-specialized методы:
func (l *Logger) Int(key string, val int) *Loggerfunc (l *Logger) String(key string, val string) *Logger(Zap и Zerolog так делают.)
Avoid closures в hot paths
Заголовок раздела «Avoid closures в hot paths»// Closure аллоцирует, если capture'ит переменныеfor i := 0; i < N; i++ { func() { fmt.Println(i) // i captured by reference }()}Лучше передавать аргументы:
for i := 0; i < N; i++ { func(i int) { fmt.Println(i) }(i)}Но даже эта function literal может escape — лучше extract named function.
Avoid range over map
Заголовок раздела «Avoid range over map»Range по map создаёт iterator state (allocates).
❌ Hot loop:
for k, v := range m { // ...}✅ Если можно — заменить на slice:
type kv struct { K string; V int }list := make([]kv, 0, len(m))for k, v := range m { list = append(list, kv{k, v}) // один раз}// потом hot iterating:for _, item := range list { // ...}Это полезно если map итерируется много раз без изменений.
Pass struct by value (carefully)
Заголовок раздела «Pass struct by value (carefully)»type Point struct { X, Y float64 }
func dist(p Point) float64 { ... } // by value — на стеке
func dist(p *Point) float64 { ... } // by pointer — pointer на стеке, но *Point может escapeМаленькие структуры (≤2 words) — по value эффективнее. Но если struct большая (>3 words) — pointer лучше (хотя возможно escape).
⚠️ Padding: struct с плохо упорядоченными полями может занимать больше места:
type Bad struct { a bool // 1 byte + 7 padding b int64 // 8 bytes c bool // 1 byte + 7 padding}// Размер: 24 bytes
type Good struct { b int64 // 8 bytes a bool // 1 byte c bool // 1 byte + 6 padding}// Размер: 16 bytesUse array vs slice если размер фиксированный
Заголовок раздела «Use array vs slice если размер фиксированный»var data [16]byte // на стеке гарантированноvar data = make([]byte, 16) // может escapeArray — value type, slice — reference (с headers).
2.3. Allocation patterns
Заголовок раздела «2.3. Allocation patterns»Object pooling
Заголовок раздела «Object pooling»sync.Pool — стандартный inst:
var bufferPool = sync.Pool{ New: func() any { return &bytes.Buffer{} },}
func handler() { buf := bufferPool.Get().(*bytes.Buffer) defer func() { buf.Reset() bufferPool.Put(buf) }() // use buf}Custom pool для специфики:
type Job struct { ID uint64 Data []byte next *Job // для linked list pool}
type JobPool struct { mu sync.Mutex head *Job}
func (p *JobPool) Get() *Job { p.mu.Lock() defer p.mu.Unlock() if p.head == nil { return &Job{} } j := p.head p.head = j.next j.next = nil return j}
func (p *JobPool) Put(j *Job) { j.ID = 0 j.Data = j.Data[:0] p.mu.Lock() j.next = p.head p.head = j p.mu.Unlock()}Custom pool сложнее, но не подвержен GC eviction (sync.Pool периодически чистится).
Arena allocators
Заголовок раздела «Arena allocators»runtime.Arena был экспериментальным в Go 1.20, но abandoned — proposal отозван из-за сложности и опасности use-after-free.
Альтернативы:
- Manual arena: большой
[]bytebuffer, ручной offset bumping - Per-request arena: аллоцировать на старте request, освобождать в конце
type Arena struct { buf []byte}
func (a *Arena) Alloc(size int) []byte { if len(a.buf)+size > cap(a.buf) { // не растим — fallback на heap return make([]byte, size) } old := len(a.buf) a.buf = a.buf[:old+size] return a.buf[old : old+size]}
func (a *Arena) Reset() { a.buf = a.buf[:0]}⚠️ Не использовать как замену GC — это для very specific patterns.
Slab allocators
Заголовок раздела «Slab allocators»Для одного типа объектов. Pre-allocate N штук, выдавать из массива.
type Slab[T any] struct { items []T free []int // индексы свободных}
func (s *Slab[T]) Acquire() *T { if len(s.free) == 0 { s.items = append(s.items, *new(T)) return &s.items[len(s.items)-1] } idx := s.free[len(s.free)-1] s.free = s.free[:len(s.free)-1] return &s.items[idx]}
func (s *Slab[T]) Release(item *T) { // вычислить индекс и добавить в free}Slab pattern полезен когда:
- Тип фиксирован
- Объекты живут долго (cache, connection pool)
- Нужен предсказуемый footprint
Stack allocation tricks
Заголовок раздела «Stack allocation tricks»Если переменная не escape — она на стеке (бесплатно). Триггеры escape:
- Возврат pointer’а
- Передача interface (boxing)
- Запись в map/slice через pointer
- Goroutine с captured variable
unsafeoperations
Минимизировать escapes:
// Escapesfunc newInt() *int { x := 42 return &x // escapes}
// Не escapesfunc sumWithCallback(cb func(int)) { x := 42 // на стеке, передаём по value cb(x)}Pattern “callback to consume” часто помогает не escape:
// Вместо возврата []byte:func ReadInto(buf []byte) int // caller предоставляет buffer2.4. Field alignment
Заголовок раздела «2.4. Field alignment»unsafe.Sizeof показывает реальный размер struct (с padding):
type S struct { A bool B int64 C bool}fmt.Println(unsafe.Sizeof(S{})) // 24 на 64-bitПравило: сортировать поля от больших к маленьким (по размеру).
fieldalignment tool
Заголовок раздела «fieldalignment tool»В golang.org/x/tools/go/analysis/passes/fieldalignment:
go install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latestfieldalignment ./...Output:
./model.go:10:6: struct of size 24 could be 16Auto-fix:
fieldalignment -fix ./...Когда важно:
- Структуры, которые аллоцируются миллионами штук
- Cache-line concerns (avoid false sharing)
- Network protocols (packed structs)
⚠️ Не оптимизировать преждевременно — для большинства типов 8 байт лишних не критичны.
2.5. Sub-tools
Заголовок раздела «2.5. Sub-tools»Старый инструмент Dave Cheney для визуализации GC. Запускает app, парсит gctrace=1 output.
В 2026 году заменяется runtime/metrics + Prometheus + Grafana.
Heap dump analysis
Заголовок раздела «Heap dump analysis»import "runtime/debug"debug.WriteHeapDump(file.Fd())Дамп в формате, который можно анализировать через external tools. Используется редко, обычно достаточно pprof.
viewcore
Заголовок раздела «viewcore»Tool для analyzing core dumps (kernel coredump или heap dump). Установка:
go install golang.org/x/debug/cmd/viewcore@latestИспользование:
viewcore corefile> goroutines> heapПолезно для post-mortem analysis (crash dump анализ).
2.6. Real case: zero-alloc JSON parsing
Заголовок раздела «2.6. Real case: zero-alloc JSON parsing»Стандартный encoding/json копирует входные данные. Альтернативы:
easyjson
Заголовок раздела «easyjson»Generator code. Создаёт MarshalJSON/UnmarshalJSON для конкретного типа.
//easyjson:jsontype User struct { ID int `json:"id"` Name string `json:"name"`}easyjson -all user.goGenerated код использует jlexer + jwriter — почти zero-alloc.
sonic (ByteDance)
Заголовок раздела «sonic (ByteDance)»JIT-based JSON парсер. Использует SIMD на amd64.
import "github.com/bytedance/sonic"sonic.Unmarshal(data, &v)Бенчмарки показывают 2-3x ускорение vs encoding/json и меньше аллокаций.
jsoniter
Заголовок раздела «jsoniter»Drop-in replacement для encoding/json:
import jsoniter "github.com/json-iterator/go"var json = jsoniter.ConfigCompatibleWithStandardLibraryjson.Unmarshal(data, &v)Чуть быстрее encoding/json, не радикально.
Benchmark comparison (типовой)
Заголовок раздела «Benchmark comparison (типовой)»| Library | ns/op | B/op | allocs/op |
|---|---|---|---|
| encoding/json | 4500 | 1200 | 28 |
| jsoniter | 3200 | 800 | 14 |
| easyjson | 1800 | 200 | 2 |
| sonic | 1100 | 80 | 1 |
(Цифры приблизительные, зависят от схемы.)
2.7. Benchmarks: zero-alloc vs allocating
Заголовок раздела «2.7. Benchmarks: zero-alloc vs allocating»// Allocating versionfunc ConcatBad(parts []string) string { s := "" for _, p := range parts { s += p } return s}
// Zero-alloc versionfunc ConcatGood(parts []string) string { var b strings.Builder n := 0 for _, p := range parts { n += len(p) } b.Grow(n) for _, p := range parts { b.WriteString(p) } return b.String()}
func BenchmarkConcatBad(b *testing.B) { parts := []string{"a", "b", "c", "d", "e"} for i := 0; i < b.N; i++ { ConcatBad(parts) }}
func BenchmarkConcatGood(b *testing.B) { parts := []string{"a", "b", "c", "d", "e"} for i := 0; i < b.N; i++ { ConcatGood(parts) }}Результат (Apple M1):
BenchmarkConcatBad-8 10000000 110 ns/op 32 B/op 4 allocs/opBenchmarkConcatGood-8 30000000 40 ns/op 8 B/op 1 allocs/op3x быстрее, 4x меньше аллокаций.
3. Gotchas (10+)
Заголовок раздела «3. Gotchas (10+)»3.1. ⚠️ sync.Pool не гарантирует retention
Заголовок раздела «3.1. ⚠️ sync.Pool не гарантирует retention»Объекты в sync.Pool могут быть очищены при любом GC. Поэтому:
- ❌ Не использовать как cache (объекты пропадут)
- ✅ Использовать как “free list” для часто-аллоцируемых объектов
- ⚠️ В Go 1.13+ pool has 2-cycle GC behavior — большинство объектов выживает 1 GC
3.2. ⚠️ unsafe.String аллиасит данные
Заголовок раздела «3.2. ⚠️ unsafe.String аллиасит данные»b := []byte("hello")s := unsafe.String(&b[0], len(b))b[0] = 'H' // UNDEFINED BEHAVIOR — изменили "immutable" string!Никогда не модифицировать []byte после конверсии в string через unsafe.
3.3. ⚠️ Interface escape
Заголовок раздела «3.3. ⚠️ Interface escape»var x int = 42var i any = x // x boxed → escape!Передача scalar в any параметр всегда аллоцирует. Это причина почему logging libraries (zap, zerolog) используют typed methods.
3.4. ⚠️ Closure capture часто escapes
Заголовок раздела «3.4. ⚠️ Closure capture часто escapes»func makeAdder(x int) func(int) int { return func(y int) int { // x captured → x на heap return x + y }}x теперь на heap, потому что closure пережил scope makeAdder.
3.5. ⚠️ append может re-allocate
Заголовок раздела «3.5. ⚠️ append может re-allocate»s := make([]int, 0, 10)s = append(s, 1, 2, 3) // в существующий cap, no allocs = append(s, ...11 more...) // exceeds cap → re-allocЕсли знаете финальный размер — pre-allocate с правильным cap.
3.6. ⚠️ string([]byte) всегда копирует
Заголовок раздела «3.6. ⚠️ string([]byte) всегда копирует»b := []byte{...}s := string(b) // КОПИЯИсключение: в case m[string(byteKey)] компилятор знает, что временная string не сохранится → optimization не копирует. Но s := string(b); m[s] будет копировать.
3.7. ⚠️ []byte(string) всегда копирует
Заголовок раздела «3.7. ⚠️ []byte(string) всегда копирует»Симметрично:
s := "hello"b := []byte(s) // КОПИЯЭто нужно потому что strings immutable. Для zero-copy — unsafe.Slice (с осторожностью).
3.8. ⚠️ Maps аллоцируют при превышении load factor
Заголовок раздела «3.8. ⚠️ Maps аллоцируют при превышении load factor»Внутри map при росте идёт rehashing (mapassign_grow). Это дискретная аллокация — иногда тратится много.
Pre-allocate с правильным hint:
m := make(map[string]int, expectedSize)3.9. ⚠️ defer аллоцирует
Заголовок раздела «3.9. ⚠️ defer аллоцирует»В Go < 1.14 каждый defer аллоцировал closure. В Go 1.14+ есть “open-coded defer” — для most cases zero-alloc. Но:
deferв цикле = многократные defer = можно превысить лимитdeferс >8 параметрами падает на slow path
Best practice: только один defer на функцию, минимум аргументов.
3.10. ⚠️ fmt.Sprintf аллоцирует много
Заголовок раздела «3.10. ⚠️ fmt.Sprintf аллоцирует много»s := fmt.Sprintf("user=%d action=%s", uid, action) // ~5 allocДля hot paths: использовать strconv.AppendInt, strconv.AppendQuote на собственный buffer.
buf := bufPool.Get().([]byte)[:0]buf = strconv.AppendInt(buf, int64(uid), 10)buf = append(buf, " action="...)buf = append(buf, action...)3.11. ⚠️ Goroutines имеют стек ~2KB initial
Заголовок раздела «3.11. ⚠️ Goroutines имеют стек ~2KB initial»Каждая goroutine = минимум 2KB stack (Go 1.4+). 1M goroutines = 2GB только на stacks (+ runtime overhead).
Если pool из миллиона short-lived goroutines — это вызывает значимый footprint.
3.12. ⚠️ Channels с буфером аллоцируют
Заголовок раздела «3.12. ⚠️ Channels с буфером аллоцируют»ch := make(chan int, 1000000) // 4MB+ allocБольшие буферизованные channels могут быть скрытым источником памяти.
3.13. ⚠️ Reflect аллоцирует
Заголовок раздела «3.13. ⚠️ Reflect аллоцирует»reflect.Value, reflect.TypeOf создают объекты на heap. Reflect-heavy code (e.g., старый encoding/json) — много аллокаций.
3.14. ⚠️ time.Now() — НЕ аллоцирует
Заголовок раздела «3.14. ⚠️ time.Now() — НЕ аллоцирует»Хорошие новости: time.Time — value type, не аллоцирует. Можно использовать в hot path без worry (но monotonic clock иногда escape’ит — реже).
3.15. ⚠️ errors.New или fmt.Errorf в hot path
Заголовок раздела «3.15. ⚠️ errors.New или fmt.Errorf в hot path»Каждый вызов errors.New("msg") аллоцирует. В hot path лучше pre-create sentinel errors:
var ErrNotFound = errors.New("not found")3.16. ⚠️ unsafe.Sizeof vs runtime.Sizes
Заголовок раздела «3.16. ⚠️ unsafe.Sizeof vs runtime.Sizes»unsafe.Sizeof — compile-time, точный размер. Но не учитывает heap-allocated parts (slice data, map internals). Реальная memory footprint больше.
4. Реальные кейсы
Заголовок раздела «4. Реальные кейсы»4.1. Cloudflare: log line zero-alloc
Заголовок раздела «4.1. Cloudflare: log line zero-alloc»Cloudflare писали о zero-alloc logging:
- Standard
logpackage аллоцирует ~10 раз на line - Их кастомный logger (под лицензией) использует sync.Pool + typed methods
- Результат: 0 alloc/log, throughput 1M+ logs/sec
Подход:
e := logger.NewEntry() // из poole.Str("user", userName)e.Int("uid", uid)e.Msg("login")// e released back to pool4.2. Uber: zap logger
Заголовок раздела «4.2. Uber: zap logger»go.uber.org/zap — production logger, zero-alloc для structured logs.
Подход:
Fieldструктуры (typed:zap.Int,zap.String)- Custom encoder pool
Logger.Check(level, msg)возвращаетnilесли level disabled — exit перед формированием fields
Бенчмарк zap vs logrus:
- zap structured: ~600 ns/op, 1 alloc/op
- logrus structured: ~13000 ns/op, 64 alloc/op
4.3. ByteDance: sonic JSON
Заголовок раздела «4.3. ByteDance: sonic JSON»ByteDance (TikTok) разработал sonic потому что:
- TikTok backend на Go обрабатывает миллиарды JSON в сутки
encoding/jsonбыла bottleneck’ом- sonic — JIT + SIMD = 2-3x speedup, 10x меньше аллокаций
- Сейчас open source, используется во многих ByteDance сервисах
4.4. Aerospike: client library
Заголовок раздела «4.4. Aerospike: client library»Aerospike Go client — пример zero-alloc paranoia:
- Все request/response объекты в sync.Pool
- Buffer pools для serialization
- Custom field alignment
- Результат: latency p99 <1ms на ops, минимальная GC pressure
4.5. Discord: read messages
Заголовок раздела «4.5. Discord: read messages»Discord (на Go) обрабатывали миллионы сообщений в секунду:
- Hot path читает channel messages из Redis
- Standard
encoding/jsonсоздавал GC pressure (15% CPU на GC) - Перешли на
easyjson+ sync.Pool для buffers - GC time упал до 3% CPU
- (Финальное решение для них — Rust, но это другая история)
4.6. CockroachDB: SQL parser
Заголовок раздела «4.6. CockroachDB: SQL parser»CockroachDB — distributed SQL DB на Go. SQL parser имеет zero-alloc:
- Tokenizer работает на []byte без копии
- AST nodes в arena (per-query)
- Reset arena после query
- Результат: parsing 100K+ queries/sec на одном CPU
5. Вопросы (25)
Заголовок раздела «5. Вопросы (25)»- Что такое escape analysis в Go? Как проверить?
- Почему zero-alloc важен для high-throughput сервисов?
- Когда оптимизация на аллокации не оправдана?
- Как использовать
go test -benchmem? Что означают B/op и allocs/op? - Какие флаги компилятора покажут escape analysis?
- Как использовать
sync.Poolправильно? - Почему
sync.Poolнельзя использовать как cache? - В чём отличие
strings.Builderот конкатенации через+? - Когда использовать
unsafe.Stringи какие риски? - Что такое interface boxing и почему вызывает аллокации?
- Почему передача
intвanyаллоцирует? - Что такое closure capture и как влияет на escape?
- Чем range over map хуже range over slice для аллокаций?
- Когда struct по value vs по pointer лучше?
- Что такое struct padding? Как минимизировать?
- Как использовать
fieldalignmenttool? - Что такое arena allocator? Почему
runtime.Arenaabandoned? - Какие альтернативы
encoding/jsonдля zero-alloc? - В чём отличие easyjson от sonic?
- Что такое bounds check elimination и как проверить?
- Почему
string([]byte)всегда копирует? - Что такое sync.Pool 2-cycle eviction?
- Как минимизировать defer overhead?
- Какие гарантии даёт
unsafe.Sliceот Go 1.20+? - Реальный кейс: вы видите в pprof что
runtime.mallocgc30% CPU. Как действовать?
6. Practice (5-8)
Заголовок раздела «6. Practice (5-8)»6.1. Найти и убрать аллокации в hot path
Заголовок раздела «6.1. Найти и убрать аллокации в hot path»Дан handler:
func handler(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) var req Request json.Unmarshal(body, &req)
resp := fmt.Sprintf(`{"id":%d,"status":"ok"}`, req.ID) w.Write([]byte(resp))}Задача: переписать на zero-alloc (или максимально близко). Замерить через benchmark.
6.2. sync.Pool для bytes.Buffer
Заголовок раздела «6.2. sync.Pool для bytes.Buffer»Написать middleware который logs request bodies. Использовать sync.Pool для bytes.Buffer, чтобы reuse’ить между запросами. Замерить улучшение через bench.
6.3. fieldalignment audit
Заголовок раздела «6.3. fieldalignment audit»Взять реальный проект (свой или open source) и пройтись fieldalignment. Найти 5+ структур, которые можно уменьшить. Замерить размер до/после.
6.4. Custom slab allocator
Заголовок раздела «6.4. Custom slab allocator»Реализовать slab allocator для Order структуры (трейдинг сценарий). Должен:
- Pre-allocate 10000 объектов
- Acquire/Release без аллокаций
- Thread-safe
Сравнить с sync.Pool по latency.
6.5. Sonic vs encoding/json benchmark
Заголовок раздела «6.5. Sonic vs encoding/json benchmark»Взять реальный JSON (10KB+), парсить через encoding/json, jsoniter, easyjson, sonic. Сравнить:
- ns/op
- B/op
- allocs/op
6.6. unsafe.String safety
Заголовок раздела «6.6. unsafe.String safety»Написать функцию BytesToString(b []byte) string через unsafe.String. Затем написать тест-кейс, который ломает immutability и приводит к UB (для понимания опасности).
6.7. Closure-free callback
Заголовок раздела «6.7. Closure-free callback»Дан код с closures в hot loop:
for _, item := range items { process(item, func() { logEvent(item.ID) })}Переписать без аллокаций closures (через interface или extracted function).
6.8. Zero-alloc HTTP handler
Заголовок раздела «6.8. Zero-alloc HTTP handler»Реализовать HTTP handler который:
- Парсит query params (без
r.URL.Query()который аллоцирует map) - Формирует JSON response без
encoding/json - 0 аллокаций на запрос
Замерить через wrk или vegeta.
7. Источники
Заголовок раздела «7. Источники»- Go Performance Patterns — https://go.dev/doc/gc-guide — официальный GC guide.
- Dave Cheney, “High Performance Go Workshop” — https://dave.cheney.net/high-performance-go-workshop
- Dmitry Vyukov, “Go scheduler” — https://dvyukov.github.io/ — runtime internals.
- Brad Fitzpatrick, “Profiling Go” — talks on GopherCon.
- ByteDance sonic — https://github.com/bytedance/sonic
- easyjson — https://github.com/mailru/easyjson
- uber-go/zap — https://github.com/uber-go/zap — zero-alloc logger.
- golang.org/x/tools/go/analysis/passes/fieldalignment — https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment
- Russ Cox, “Escape Analysis” — https://research.swtch.com/ — детальный разбор.
- Aerospike Go client — https://github.com/aerospike/aerospike-client-go — real-world example.
- CockroachDB performance blog — https://www.cockroachlabs.com/blog/
- Cloudflare Go blog — https://blog.cloudflare.com/tag/go/ — production case studies.