Senior Go Interview Prep - Core Go: https://go.vbloher.org/docs/01-core-go/ - Механика defer в Go: https://go.vbloher.org/docs/01-core-go/defer/ - Встраивание структур и интерфейсов (Embedding): https://go.vbloher.org/docs/01-core-go/embedding/ - Ошибки в Go: error, wrapping, errors.Is/As/Join: https://go.vbloher.org/docs/01-core-go/errors/ - Дженерики в Go (1.18+): https://go.vbloher.org/docs/01-core-go/generics/ - Интерфейсы в Go: https://go.vbloher.org/docs/01-core-go/interfaces/ - Устройство map в Go: https://go.vbloher.org/docs/01-core-go/maps/ - panic / recover: механика, раскрутка стека и runtime-паники: https://go.vbloher.org/docs/01-core-go/panic-recover/ - Указатели в Go: https://go.vbloher.org/docs/01-core-go/pointers/ - Рефлексия в Go (reflect): https://go.vbloher.org/docs/01-core-go/reflection/ - Внутреннее устройство слайсов в Go: https://go.vbloher.org/docs/01-core-go/slices/ - Строки, руны и байты в Go: https://go.vbloher.org/docs/01-core-go/strings-runes-bytes/ - Система типов Go: defined types, alignment, memory layout: https://go.vbloher.org/docs/01-core-go/type-system/ - Concurrency: https://go.vbloher.org/docs/02-concurrency/ - sync/atomic: https://go.vbloher.org/docs/02-concurrency/atomic/ - Буферизованные vs небуферизованные каналы: https://go.vbloher.org/docs/02-concurrency/buffered-unbuffered/ - Канал vs Mutex: когда что выбрать: https://go.vbloher.org/docs/02-concurrency/channel-vs-mutex/ - Каналы: устройство hchan: https://go.vbloher.org/docs/02-concurrency/channels/ - Утечки горутин, дедлоки, livelock, starvation: https://go.vbloher.org/docs/02-concurrency/common-leaks-deadlocks/ - sync.Cond: https://go.vbloher.org/docs/02-concurrency/cond/ - context: https://go.vbloher.org/docs/02-concurrency/context/ - Горутины: жизненный цикл, стоимость, стек: https://go.vbloher.org/docs/02-concurrency/goroutines-lifecycle/ - sync.Mutex и sync.RWMutex: https://go.vbloher.org/docs/02-concurrency/mutex-rwmutex/ - sync.Once: https://go.vbloher.org/docs/02-concurrency/once/ - Паттерны конкурентности: https://go.vbloher.org/docs/02-concurrency/patterns/ - Race Detector (гонки данных и -race): https://go.vbloher.org/docs/02-concurrency/race-detector/ - Планировщик GMP: https://go.vbloher.org/docs/02-concurrency/scheduler-gmp/ - select: https://go.vbloher.org/docs/02-concurrency/select/ - sync.WaitGroup: https://go.vbloher.org/docs/02-concurrency/waitgroup/ - Runtime и память: https://go.vbloher.org/docs/03-runtime-memory/ - Паттерны аллокаций и снижение давления на GC: https://go.vbloher.org/docs/03-runtime-memory/allocation-patterns/ - Escape Analysis: когда переменная убегает в кучу: https://go.vbloher.org/docs/03-runtime-memory/escape-analysis/ - Сборщик мусора Go: concurrent tri-color mark-sweep: https://go.vbloher.org/docs/03-runtime-memory/gc/ - Тюнинг GC: GOGC и GOMEMLIMIT: https://go.vbloher.org/docs/03-runtime-memory/gogc-gomemlimit/ - GOMAXPROCS: параллелизм планировщика и проблема контейнеров: https://go.vbloher.org/docs/03-runtime-memory/gomaxprocs/ - Утечки горутин (goroutine leaks): https://go.vbloher.org/docs/03-runtime-memory/goroutine-leaks/ - Утечки памяти в Go (несмотря на GC): https://go.vbloher.org/docs/03-runtime-memory/memory-leaks/ - Модель памяти Go (Go Memory Model): happens-before и синхронизация: https://go.vbloher.org/docs/03-runtime-memory/memory-model/ - pprof: профилирование CPU, памяти и блокировок в Go: https://go.vbloher.org/docs/03-runtime-memory/pprof/ - Execution Tracer и runtime/trace: тайминги вместо агрегатов: https://go.vbloher.org/docs/03-runtime-memory/runtime-tracing/ - Стек vs Куча: где живут данные в Go: https://go.vbloher.org/docs/03-runtime-memory/stack-vs-heap/ - Тестирование: https://go.vbloher.org/docs/04-testing/ - testify, assert/require и golden files: https://go.vbloher.org/docs/04-testing/assertions-testify/ - Бенчмарки в Go: https://go.vbloher.org/docs/04-testing/benchmarks/ - Покрытие, -race и флаки-тесты: https://go.vbloher.org/docs/04-testing/coverage-race/ - Нативный fuzzing в Go (1.18+): https://go.vbloher.org/docs/04-testing/fuzzing/ - Интеграционные тесты, testcontainers-go, TestMain: https://go.vbloher.org/docs/04-testing/integration-testcontainers/ - Моки, стабы и тестируемость: https://go.vbloher.org/docs/04-testing/mocks/ - Table-driven тесты, subtests и параллельность: https://go.vbloher.org/docs/04-testing/table-driven/ - Backend: https://go.vbloher.org/docs/05-backend/ - Аутентификация и авторизация: AuthN/AuthZ, сессии vs токены, RBAC/ABAC, API keys, mTLS, секреты: https://go.vbloher.org/docs/05-backend/auth-authz/ - Graceful Shutdown HTTP/gRPC сервера в Go: https://go.vbloher.org/docs/05-backend/graceful-shutdown/ - gRPC: типы RPC, интерсепторы, контекст, метаданные, error model: https://go.vbloher.org/docs/05-backend/grpc/ - JWT (JSON Web Token): https://go.vbloher.org/docs/05-backend/jwt/ - Middleware-паттерн в Go: https://go.vbloher.org/docs/05-backend/middleware/ - net/http: Server, Handler, ServeMux, таймауты, Client и контекст: https://go.vbloher.org/docs/05-backend/net-http/ - OAuth2: роли, grant types, OIDC, токены и типовые ошибки: https://go.vbloher.org/docs/05-backend/oauth2/ - OpenAPI/Swagger, code generation, contract-first vs code-first, валидация: https://go.vbloher.org/docs/05-backend/openapi/ - Protocol Buffers: схемы, wire format, эволюция и совместимость: https://go.vbloher.org/docs/05-backend/protobuf/ - REST: принципы, версионирование, идемпотентность, статусы, пагинация, ошибки: https://go.vbloher.org/docs/05-backend/rest/ - Сети и протоколы: https://go.vbloher.org/docs/06-networking/ - Пулы соединений: http.Transport, БД, утечки: https://go.vbloher.org/docs/06-networking/connection-pooling/ - DNS: записи, резолвинг, кэширование, DNS в Go: https://go.vbloher.org/docs/06-networking/dns/ - Версии HTTP: 1.1, 2, 3: https://go.vbloher.org/docs/06-networking/http-versions/ - TCP/IP: модель, транспорт и что важно бэкендеру: https://go.vbloher.org/docs/06-networking/tcp-ip/ - TLS: handshake, сертификаты, mTLS, производительность: https://go.vbloher.org/docs/06-networking/tls/ - UDP и надёжность поверх UDP: https://go.vbloher.org/docs/06-networking/udp/ - WebSocket: upgrade, фреймы, масштабирование: https://go.vbloher.org/docs/06-networking/websocket/ - Базы данных: https://go.vbloher.org/docs/07-databases/ - Пул соединений к PostgreSQL в Go: database/sql, pgx, pgxpool, PgBouncer: https://go.vbloher.org/docs/07-databases/connection-pooling-pgx/ - Взаимоблокировки (Deadlocks) в PostgreSQL: https://go.vbloher.org/docs/07-databases/deadlocks/ - Индексы в PostgreSQL: https://go.vbloher.org/docs/07-databases/indexes/ - Уровни изоляции транзакций в PostgreSQL: https://go.vbloher.org/docs/07-databases/isolation-levels/ - MVCC в PostgreSQL: версии строк, видимость, VACUUM и bloat: https://go.vbloher.org/docs/07-databases/mvcc/ - Обзор NoSQL и Redis: https://go.vbloher.org/docs/07-databases/nosql-redis/ - Партиционирование таблиц в PostgreSQL: https://go.vbloher.org/docs/07-databases/partitioning/ - Архитектура PostgreSQL: https://go.vbloher.org/docs/07-databases/postgresql-architecture/ - Планирование и оптимизация запросов в PostgreSQL: https://go.vbloher.org/docs/07-databases/query-planning/ - Репликация в PostgreSQL: https://go.vbloher.org/docs/07-databases/replication/ - Шардирование (горизонтальное масштабирование): https://go.vbloher.org/docs/07-databases/sharding/ - Транзакции в PostgreSQL и Go (database/sql, pgx): https://go.vbloher.org/docs/07-databases/transactions/ - Распределённые системы: https://go.vbloher.org/docs/08-distributed-systems/ - CAP теорема: https://go.vbloher.org/docs/08-distributed-systems/cap-theorem/ - Circuit Breaker: https://go.vbloher.org/docs/08-distributed-systems/circuit-breaker/ - Консенсус и Raft: репликация состояния в присутствии отказов: https://go.vbloher.org/docs/08-distributed-systems/consensus-raft/ - Модели согласованности: https://go.vbloher.org/docs/08-distributed-systems/consistency/ - Гарантии доставки сообщений: at-most-once / at-least-once / exactly-once: https://go.vbloher.org/docs/08-distributed-systems/delivery-guarantees/ - Eventual Consistency: https://go.vbloher.org/docs/08-distributed-systems/eventual-consistency/ - Идемпотентность в распределённых системах: https://go.vbloher.org/docs/08-distributed-systems/idempotency/ - Apache Kafka: https://go.vbloher.org/docs/08-distributed-systems/kafka/ - Transactional Outbox: https://go.vbloher.org/docs/08-distributed-systems/outbox/ - RabbitMQ: AMQP 0-9-1, маршрутизация, надёжность доставки и сравнение с Kafka: https://go.vbloher.org/docs/08-distributed-systems/rabbitmq/ - Ретраи: backoff, jitter, budgets и идемпотентность: https://go.vbloher.org/docs/08-distributed-systems/retries/ - Saga Pattern: https://go.vbloher.org/docs/08-distributed-systems/saga/ - Observability: https://go.vbloher.org/docs/09-observability/ - Grafana: https://go.vbloher.org/docs/09-observability/grafana/ - Метрики: RED, USE, Golden Signals: https://go.vbloher.org/docs/09-observability/metrics/ - OpenTelemetry: https://go.vbloher.org/docs/09-observability/opentelemetry/ - Prometheus: https://go.vbloher.org/docs/09-observability/prometheus/ - SLI / SLO / SLA: https://go.vbloher.org/docs/09-observability/slo-sli/ - Структурированное логирование (slog): https://go.vbloher.org/docs/09-observability/structured-logging/ - Distributed Tracing: https://go.vbloher.org/docs/09-observability/tracing/ - System Design: https://go.vbloher.org/docs/10-system-design/ - Analytics Pipeline: https://go.vbloher.org/docs/10-system-design/analytics-pipeline/ - Chat System: https://go.vbloher.org/docs/10-system-design/chat/ - Фреймворк System Design интервью: https://go.vbloher.org/docs/10-system-design/framework/ - Notification Service: https://go.vbloher.org/docs/10-system-design/notification-service/ - Order Service: https://go.vbloher.org/docs/10-system-design/order-service/ - Payment Service: https://go.vbloher.org/docs/10-system-design/payment-service/ - Rate Limiter: https://go.vbloher.org/docs/10-system-design/rate-limiter/ - URL Shortener: https://go.vbloher.org/docs/10-system-design/url-shortener/ - DevOps: https://go.vbloher.org/docs/11-devops/ - CI/CD: пайплайны, стадии, стратегии деплоя: https://go.vbloher.org/docs/11-devops/cicd/ - Облака (AWS / GCP) для бэкендера: https://go.vbloher.org/docs/11-devops/cloud-aws-gcp/ - Docker для Go-разработчика: https://go.vbloher.org/docs/11-devops/docker/ - GitHub Actions и GitLab CI: https://go.vbloher.org/docs/11-devops/github-gitlab-ci/ - Kubernetes для Go-разработчика: https://go.vbloher.org/docs/11-devops/kubernetes/ - Terraform / Infrastructure as Code: https://go.vbloher.org/docs/11-devops/terraform/ - Алгоритмы: https://go.vbloher.org/docs/12-algorithms/ - Типовые алгоритмические задачи и паттерны: https://go.vbloher.org/docs/12-algorithms/common-problems/ - Асимптотическая сложность (Big-O): https://go.vbloher.org/docs/12-algorithms/complexity/ - Структуры данных в Go: https://go.vbloher.org/docs/12-algorithms/data-structures/ - Специфика live-coding на Go: https://go.vbloher.org/docs/12-algorithms/go-specifics/ - Behavioral: https://go.vbloher.org/docs/13-behavioral/ - Конфликты, разногласия и работа со стейкхолдерами: https://go.vbloher.org/docs/13-behavioral/conflicts/ - Как проходит senior-интервью: этапы, оценка, оффер: https://go.vbloher.org/docs/13-behavioral/interview-flow/ - Лидерство и менторство: https://go.vbloher.org/docs/13-behavioral/leadership-mentoring/ - Типовые поведенческие вопросы для Senior: https://go.vbloher.org/docs/13-behavioral/senior-questions/ > Модуль: Runtime и память · Уровень: Senior+ ## TL;DR Большинство «горячих» проблем производительности в Go — это не CPU, а аллокации: каждая аллокация в куче нагружает аллокатор и увеличивает работу GC (assist, mark, scan). Senior-разработчик умеет считать аллокации через `-benchmem`, предаллоцировать слайсы/мапы с учётом amortized growth, переиспользовать буферы (`strings.Builder`, `bytes.Buffer`, `sync.Pool`) и избегать неявного boxing в интерфейсах. Главное — измерять (`pprof`, `-benchmem`, `escape analysis`), а не угадывать. ## Теория ### Стоимость аллокации и связь с GC В Go стоимость одной аллокации складывается из нескольких частей: 1. **Сам акт выделения** — попадание в size class, поиск свободного span в `mcache` (per-P, без блокировок), при промахе — в `mcentral` (с lock), при промахе — в `mheap`. 2. **GC mark/scan** — каждый живой объект с указателями нужно просканировать на этапе mark. Чем больше объектов и указателей, тем дольше mark-фаза и тем чаще она запускается. 3. **GC assist** — если горутина аллоцирует быстрее, чем GC успевает помечать, рантайм заставляет саму горутину выполнять работу маркировки (mutator assist). Это видно как «непонятные» паузы прямо в горячем коде. 4. **Триггер GC** — по умолчанию `GOGC=100`: GC стартует, когда heap вырос вдвое относительно live set после предыдущего цикла. Больше аллокаций ⇒ чаще GC. Ключевая идея: **уменьшение количества и размера аллокаций в куче снижает частоту GC и объём scan-работы.** Стек — бесплатный (освобождается при возврате из функции), куча — нет. ```go // runtime.MemStats показывает кумулятивные аллокации var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("Mallocs=%d Frees=%d HeapAlloc=%d NumGC=%d\n", m.Mallocs, m.Frees, m.HeapAlloc, m.NumGC) ``` ### Escape analysis: куда попадёт объект Компилятор решает, живёт ли объект на стеке или убегает (escape) в кучу. Если убегает — это аллокация. ```bash go build -gcflags='-m -m' ./... # Типичные сообщения: # ./x.go:10:6: moved to heap: x # ./x.go:12:13: ... argument does not escape # ./x.go:15:9: &T{...} escapes to heap ``` Частые причины escape: - возврат указателя на локальную переменную; - сохранение указателя в поле структуры/слайса, живущей дольше функции; - передача значения в `interface{}` (boxing); - размер заранее неизвестен компилятору (slice с динамической длиной, замыкания, захватывающие переменные); - вызовы через интерфейс, которые компилятор не может девиртуализировать. ```go func stackAlloc() int { x := 42 // остаётся на стеке return x } func heapAlloc() *int { x := 42 // moved to heap: возвращаем указатель return &x } ``` ### Предаллокация слайсов: make с cap Рост слайса через `append` амортизированно O(1), но каждый рост — это **новый backing array + копирование старых элементов**. Если итоговый размер известен, задавайте `cap`. ```go // Плохо: несколько реаллокаций по мере роста func bad(n int) []int { var s []int for i := 0; i < n; i++ { s = append(s, i) // реаллокации: 0->1->2->4->8->... } return s } // Хорошо: одна аллокация func good(n int) []int { s := make([]int, 0, n) // len=0, cap=n for i := 0; i < n; i++ { s = append(s, i) } return s } ``` Важно различать: - `make([]int, n)` — len=n, cap=n, элементы занулены, `append` будет добавлять **после** n. - `make([]int, 0, n)` — len=0, cap=n; правильный вариант для построения через `append`. Типичная ошибка — `make([]T, n)` вместо `make([]T, 0, n)` с последующим `append`, что даёт двойной размер и «фантомные» нулевые элементы в начале. ### Growth factor: 2x / 1.25x Стратегия роста слайса в современных версиях Go (Go 1.18+, runtime `growslice`): | Текущий cap | Множитель роста (приблизительно) | |-------------|----------------------------------| | < 256 элементов | ×2 | | ≥ 256 элементов | ×1.25 (плавный переход, growth = oldcap + (oldcap+3*256)/4) | Раньше (до 1.18) порог был «удвоение до 1024, потом ×1.25». Формула для больших слайсов сглажена, чтобы коэффициент мягко стремился к 1.25, а не прыгал. После вычисления нужной ёмкости результат **округляется вверх до размера size class** аллокатора, поэтому реальный `cap` часто больше расчётного. ```go s := make([]int, 0) prev := cap(s) for i := 0; i < 2000; i++ { s = append(s, i) if cap(s) != prev { fmt.Printf("len=%d cap=%d\n", len(s), cap(s)) prev = cap(s) } } // Видно скачки cap: 1,2,4,8,...,256,512,848,... (округлено по size class) ``` Амортизация: суммарное копирование при росте с 0 до N — это O(N), потому что геометрический ряд `N + N/2 + N/4 + ... = 2N`. Поэтому каждый отдельный `append` амортизированно O(1), но предаллокация устраняет эти 2N копирований и промежуточные аллокации. ### Предаллокация мап: make с size hint `make(map[K]V, hint)` заранее выделяет достаточно бакетов, чтобы вместить `hint` элементов без рехеширования. ```go m := make(map[string]int, 1000) // избегаем серии grow/rehash ``` Под капотом (классическая реализация до swissmap в Go 1.24): мапа — это массив бакетов по 8 слотов. При превышении load factor (~6.5 на бакет) происходит **incremental growth**: выделяется вдвое больше бакетов, и элементы постепенно эвакуируются. Hint позволяет сразу выбрать нужное число бакетов. В Go 1.24+ map реализована на SwissTable, но семантика size hint сохранилась — она по-прежнему снижает число grow-операций. Замечание: hint — это **подсказка**, а не жёсткая ёмкость. Мапа всё равно вырастет при необходимости. ### sync.Pool: устройство и жизненный цикл `sync.Pool` — пул временных объектов для переиспользования между горутинами, чтобы снизить число аллокаций и нагрузку на GC. ```go var bufPool = sync.Pool{ New: func() any { return new(bytes.Buffer) }, } func handle(data []byte) { buf := bufPool.Get().(*bytes.Buffer) buf.Reset() // ОБЯЗАТЕЛЬНО: объект из пула «грязный» defer bufPool.Put(buf) buf.Write(data) // ... используем buf } ``` **Под капотом:** - **Per-P pools.** У каждого логического процессора `P` (их `GOMAXPROCS`) есть приватная ячейка (`private`) и сдвиговый дек (`shared`). `Get`/`Put` сначала работают с локальным `P` без блокировок — это и даёт масштабируемость. Только при промахе горутина «крадёт» (work-stealing) объекты из shared-деков других `P`. - **Pin к P.** На время `Get`/`Put` горутина пиннится к `P` (`runtime_procPin`), чтобы избежать гонок при миграции. - **Очистка при GC.** В начале каждого GC рантайм вызывает `poolCleanup`. **Pool не удерживает объекты от сборки между циклами GC.** Это сделано специально, чтобы пул не превращался в источник утечки. - **Victim cache (Go 1.13+).** Чтобы один GC не обнулял весь пул резко (что вызывало всплеск аллокаций сразу после GC), введён двухуровневый механизм: - При очистке текущие объекты не выбрасываются сразу, а перемещаются в `victim` (жертвенный) кэш. - `Get` при промахе в основном кэше пробует достать из `victim`. - На следующем GC `victim` очищается окончательно, а текущие объекты снова переезжают в `victim`. - Итог: объект живёт **до двух циклов GC**, что сглаживает провалы hit-rate. ``` GC #1: live -> victim, New victim primary = old primary GC #2: victim -> GC, live -> victim ``` **Когда sync.Pool НЕ помогает (или вредит):** - **Объекты разного размера.** Пул отдаёт случайный объект; если вы кладёте слайсы разной capacity, можно получить маленький буфер и снова реаллоцировать его, либо удерживать огромные буферы (нужна стратегия отсева больших объектов). - **Долгоживущие объекты.** Pool предназначен для **короткоживущих временных** объектов в рамках запроса/операции. Для кэширования с контролируемым TTL он не годится — GC сбросит его непредсказуемо. - **Низкая частота переиспользования.** Если объект берётся редко, GC успеет очистить пул между использованиями и `New` будет вызываться всё равно. - **Маленькие объекты.** Накладные расходы `Get`/`Put` (pin, type assertion) могут превысить выгоду от устранения дешёвой аллокации. Для крошечных структур stack allocation быстрее. - **Хранение объектов с указателями наружу.** Если вы кладёте в пул структуру, ссылающуюся на большой граф, вы продлеваете его жизнь. Классическое правило: **обнуляйте/Reset объект перед Put или после Get** — иначе утечёт старое содержимое (security-риск) или будут баги. ### strings.Builder и bytes.Buffer Конкатенация строк через `+` или `+=` в цикле — антипаттерн: строки иммутабельны, каждая операция аллоцирует новую строку. ```go // Плохо: O(n^2) аллокаций и копирований func concatBad(parts []string) string { s := "" for _, p := range parts { s += p } return s } // Хорошо: амортизированный рост, одна финальная строка func 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() // String() не копирует backing array } ``` `strings.Builder.String()` использует `unsafe`, чтобы вернуть строку **без копирования** backing-байтов — это его главное преимущество перед `bytes.Buffer.String()` (который копирует). Builder запрещает копирование себя (`noCopy`) после первого использования. **Reuse `bytes.Buffer`** через пул — типовой паттерн (см. sync.Pool выше). Не забывайте `Reset()`. ```go var b bytes.Buffer for _, job := range jobs { b.Reset() // переиспользуем backing array encode(&b, job) send(b.Bytes()) // ВНИМАНИЕ: Bytes() возвращает срез на внутренний буфер } ``` ### Boxing в interface{} Помещение значения в интерфейс (`interface{}`/`any`) часто вызывает аллокацию: интерфейс хранит (type, pointer), и для не-указательного значения нужно где-то разместить данные. ```go func box(x int) any { return x } // int escapes to heap (boxing) ``` Исключения/оптимизации: - Рантайм кэширует **малые целые** (0–255): `staticuint64s` — boxing `byte`/малого int не аллоцирует. - Помещение **указателя** в интерфейс не аллоцирует (указатель и есть данные). - `fmt.Println(args ...any)` боксит каждый аргумент — горячий путь логирования может удивить аллокациями. ```go // Плохо в горячем пути: каждый x боксится var sink []any for i := 0; i < n; i++ { sink = append(sink, i) // n boxing-аллокаций } ``` ### Value vs pointer receivers и аллокации Выбор receiver влияет и на копирование, и на escape: - **Value receiver** копирует значение при каждом вызове. Для больших структур это дорого по CPU, но **не аллоцирует** (копия на стеке). - **Pointer receiver** не копирует, но если значение нужно передать через интерфейс — оно **escape в кучу** (интерфейс должен хранить указатель на живущий объект). ```go type Big struct{ data [1024]byte } func (b Big) ValSum() int { /* копия 1KB на стеке */ } func (b *Big) PtrSum() int { /* без копии */ } var x Big var i interface{ ValSum() int } = x // КОПИЯ x уезжает в кучу (boxing значения) var j interface{ PtrSum() int } = &x // &x escape, сам x может остаться ``` Правила для senior: - Консистентность набора методов (не мешать value/pointer без причины). - Большие структуры — pointer receiver (избежать копий). - Если метод мутирует — pointer receiver обязателен. - Помните: присваивание значения в интерфейс копирует/боксит; pointer receiver + interface ⇒ escape объекта. ### append pitfalls и аллокации ```go // 1. Алиасинг backing array a := make([]int, 3, 5) b := append(a, 99) // cap хватает -> b делит backing с a! b[0] = -1 // a[0] тоже стал -1 // 2. Скрытая аллокация при превышении cap c := append(a, 1, 2, 3) // cap не хватает -> новый массив, c независим от a // 3. append(nil-slice...) — корректно, аллоцирует при первом элементе var d []int d = append(d, 1) // ок ``` Гарантированно скопировать (отвязать от backing): ```go dst := make([]int, len(src)) copy(dst, src) // или (Go 1.21+): dst := slices.Clone(src) ``` ## Подводные камни / gotchas - `make([]T, n)` + `append` вместо `make([]T, 0, n)` — двойной размер и нулевые «хвосты» в начале. - `b := append(a, x)` при достаточном cap молча мутирует backing array `a` — источник трудноуловимых багов в конкурентном коде. - `sync.Pool` не зануляет объекты — забытый `Reset()` ведёт к утечке данных между запросами. - `sync.Pool` нельзя использовать как кэш с гарантиями: GC очистит его в любой момент. - `bytes.Buffer.Bytes()` и `strings.Builder` через unsafe возвращают срез/строку на внутренний буфер — нельзя удерживать после `Reset()`/повторного использования. - Boxing в `any` в горячем цикле (логи, метрики) — невидимый источник аллокаций; ловится только `-benchmem`/escape analysis. - Предаллокация по слишком большому `cap` тратит память зря и может удерживать большой backing через под-слайс. - Value receiver на большой структуре в горячем пути — тихий CPU-killer (копирование), но не аллокация; pointer receiver + interface — наоборот, аллокация. - `b.Grow(n)` у Builder/Buffer надо звать ДО записи, иначе реаллокации уже случились. ## Вопросы на собеседовании **В:** Чем `make([]int, 0, n)` отличается от `make([]int, n)` и почему это важно при построении слайса через `append`? **О:** Первый создаёт слайс len=0, cap=n — `append` будет заполнять заранее выделенный backing без реаллокаций до n элементов. Второй создаёт len=n, cap=n из n занулённых элементов, и `append` начнёт добавлять **после** них, давая фактический размер 2n и нулевой «хвост» в начале. При построении через `append` правильный вариант — `make([]T, 0, n)`: одна аллокация и корректный результат. **В:** Как работает рост слайса и какой growth factor в современном Go? **О:** При нехватке cap `append` вызывает `growslice`: вычисляется новая ёмкость (~×2 пока cap < 256, и плавно ×1.25 для больших), результат округляется вверх до size class аллокатора, выделяется новый массив и копируются старые элементы. Амортизированно append — O(1), потому что суммарное копирование с 0 до N равно ~2N (геометрический ряд). Предаллокация устраняет промежуточные аллокации и копирования. **В:** Объясните устройство `sync.Pool`: per-P пулы и victim cache. **О:** У каждого `P` есть локальная приватная ячейка и shared-дек; `Get`/`Put` сначала работают с локальным `P` без блокировок, при промахе крадут из shared других `P`. В начале каждого GC `poolCleanup` очищает пул, но не сразу: текущие объекты переезжают в `victim` cache, который `Get` проверяет при промахе. На следующем GC victim очищается окончательно. Так объект живёт до двух циклов GC — это сглаживает всплеск аллокаций сразу после сборки. Pool намеренно не удерживает объекты надолго, чтобы не становиться источником утечки. **В:** Когда `sync.Pool` НЕ даёт выигрыша или вредит? **О:** Для долгоживущих объектов (GC сбросит непредсказуемо — это не кэш), при низкой частоте переиспользования (GC успевает очистить, `New` вызывается всё равно), для очень маленьких/дешёвых объектов (overhead на pin и type assertion перекрывает выгоду), при объектах разной capacity (можно получить маленький буфер и реаллоцировать, либо удерживать большие). Также опасен без `Reset` — утечка данных между запросами. **В:** Почему конкатенация строк через `+=` в цикле — антипаттерн, и чем `strings.Builder` лучше `bytes.Buffer`? **О:** Строки иммутабельны, `+=` каждый раз аллоцирует новую строку и копирует — O(n²). `strings.Builder` амортизированно растит внутренний `[]byte` и возвращает строку без копирования (через `unsafe`), тогда как `bytes.Buffer.String()` копирует backing. Идеально — `b.Grow(totalLen)` заранее, чтобы избежать всех реаллокаций. **В:** Что такое boxing в интерфейс и когда он не аллоцирует? **О:** Интерфейс хранит пару (тип, указатель на данные); чтобы поместить не-указательное значение, рантайму нужно разместить данные — обычно в куче (escape). Не аллоцирует: помещение указателя (он сам и есть данные) и малые целые 0–255 (кэш `staticuint64s`). Горячие места — `fmt`-вызовы и логирование с `...any`. **В:** Как value vs pointer receiver влияет на аллокации? **О:** Value receiver копирует значение (на стеке, без аллокации, но дорого по CPU для больших структур). При присваивании значения в интерфейс копия боксится и уезжает в кучу. Pointer receiver не копирует, но `&x` в интерфейсе вызывает escape объекта. Правило: большие/мутирующие — pointer; помнить, что интерфейс + значение = boxing с аллокацией. **В:** В чём опасность `append`, делящего backing array? **О:** Если в исходном слайсе хватает cap, `append` пишет в тот же backing, и новый слайс делит память со старым — мутация одного видна в другом. В конкурентном коде или при передаче под-слайсов это даёт трудноуловимые баги. Решение — `slices.Clone` или явный `make`+`copy`, либо full-slice expression `a[low:high:max]`, ограничивающий cap. **В:** Как измерить аллокации в бенчмарке? **О:** `go test -bench=. -benchmem` выводит `B/op` (байт на операцию) и `allocs/op` (число аллокаций). Дополнительно — escape analysis (`-gcflags='-m'`), `runtime.ReadMemStats`, `pprof` heap-профиль. Senior смотрит именно на `allocs/op`: ноль аллокаций в горячем пути — частая цель оптимизации. ```go func BenchmarkConcat(b *testing.B) { parts := []string{"a", "b", "c", "d"} b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { var sb strings.Builder for _, p := range parts { sb.WriteString(p) } _ = sb.String() } } ``` ```bash go test -bench=BenchmarkConcat -benchmem # BenchmarkConcat-10 10000000 45 ns/op 8 B/op 1 allocs/op ``` ## На что копают на senior+ - Умение читать вывод `-gcflags='-m -m'` и объяснить, **почему** конкретная переменная escape (возврат указателя, boxing, неизвестный размер, замыкание). - Понимание связи аллокаций с GC: assist, частота циклов через `GOGC`/`GOMEMLIMIT`, влияние числа указателей на scan-фазу. - Точное устройство `sync.Pool`: per-P shared-деки, work stealing, pin к P, victim cache, и **почему** Pool не годится для кэширования. - Знание формулы роста слайса и того, что cap округляется по size class; умение посчитать амортизацию. - Различие `make([]T, n)` vs `make([]T, 0, n)` и full-slice expression `a[i:j:k]` для контроля cap и предотвращения алиасинга. - Понимание, что `Builder.String()`/`Buffer.Bytes()` отдают вид на внутренний буфер, и связанные lifetime-риски. - Способность спроектировать пул буферов с отсевом слишком больших объектов и корректным `Reset`. - Знание изменения map на SwissTable (Go 1.24) и сохранения семантики size hint.