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 memory model определяет, при каких условиях чтение переменной в одной горутине **гарантированно видит** значение, записанное в другой. Основа — отношение **happens-before**: если запись happens-before чтения (и между ними нет других записей в эту переменную), чтение видит эту запись. Гарантии дают **только** примитивы синхронизации (channels, sync.Mutex, sync.Once, sync/atomic, WaitGroup). **Программа без data race** ведёт себя как sequential consistency; программа с гонкой имеет неопределённое поведение, и компилятор/CPU вправе переупорядочивать операции. С Go 1.19 модель официально переписана и формализует `sync/atomic` как sequentially consistent. ## Теория ### Что вообще гарантирует модель памяти Цитата из официального документа (go.dev/ref/mem, ревизия 2022): > «The Go memory model specifies the conditions under which reads of a variable in one goroutine can be guaranteed to observe values produced by writes to the same variable in a different goroutine.» И ключевое прагматичное правило оттуда же — **The Advice**: > «Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access. To serialize access, protect the data with channel operations or other synchronization primitives such as those in the `sync` and `sync/atomic` packages. If you must read the rest of this document to understand the behavior of your program, you are being too clever. Don't be clever.» Иными словами: без синхронизации нет гарантий видимости. Внутри **одной** горутины код исполняется так, как написан (single-goroutine sequential consistency), но **между** горутинами порядок не определён, пока не установлено happens-before. ### Memory operations и data race (формализация 1.19) В новой редакции модель оперирует понятием memory operation, у которой есть: тип (read/write/sync), адрес, прочитанное/записанное значение. Программа описывается множеством исполнений; корректное исполнение требует, чтобы каждое чтение «видело» допустимую запись. **Определение data race** (официальное): > «A data race is defined as a write to a memory location happening concurrently with another read or write to that same location, unless all the accesses involved are atomic data accesses as defined by the sync/atomic package.» Две операции **конкурентны**, если ни одна не happens-before другой. Если есть гонка на не-atomic переменной — поведение программы по модели не определено (с практической оговоркой ниже про «no out-of-thin-air», memory safety и т.п.). ### Happens-before: формальное отношение Happens-before — это **частичный порядок** на memory operations. Базовые правила: 1. **Program order (within a goroutine)**: если операция A текстуально предшествует B в одной горутине, то для целей этой горутины A happens-before B. (Компилятор/CPU могут переупорядочивать, но только пока это не наблюдаемо в рамках одной горутины.) 2. **Транзитивность**: A hb B и B hb C ⇒ A hb C. 3. **Synchronization edges**: примитивы синхронизации создают рёбра happens-before **между** горутинами (см. ниже channels/mutex/once/atomic). Главная теорема видимости: > Чтение `r` переменной `v` **видит** запись `w` в `v`, если: (1) `w` happens-before `r`, и (2) нет другой записи `w'` в `v` такой, что `w` happens-before `w'` happens-before `r`. Если же `r` и `w` конкурентны (нет hb ни в одну сторону) — `r` может увидеть, а может и не увидеть `w`; это data race. ### Channels — правила happens-before Каналы — основной идиоматичный механизм синхронизации в Go. Правила из модели: ``` 1. A send on a channel happens-before the corresponding receive completes. 2. The closing of a channel happens-before a receive that returns a zero value because the channel is closed. 3. (Unbuffered) A receive from an unbuffered channel happens-before the send on that channel completes. 4. (Buffered, capacity C) The kth receive on a channel with capacity C happens-before the (k+C)th send completes. ``` Правило 1 — основа передачи данных: всё, что горутина-отправитель записала **до** send, видно получателю **после** receive. ```go var data int // обычная (не atomic) переменная done := make(chan struct{}) go func() { data = 42 // (1) запись close(done) // (2) close happens-after (1) в program order }() <-done // (3) receive zero value — happens-after close (правило 2) fmt.Println(data) // гарантированно 42: (1) hb (2) hb (3) hb (этот read) ``` Правило 3 (unbuffered) часто удивляет: на **небуферизованном** канале receive happens-before завершения send. Это позволяет использовать unbuffered-канал как «рукопожатие» в обе стороны. ```go c := make(chan int) // unbuffered var a string go func() { a = "hello" // запись c <- 0 // send; завершится только ПОСЛЕ того как receive начался }() <-c // receive happens-before завершения send выше print(a) // гарантированно "hello" ``` > Тонкая ошибка из самой спецификации: если канал **буферизованный**, такая гарантия в обратную сторону НЕ держится, и аналогичный код может вывести пустую строку. Различие unbuffered/buffered здесь принципиально. ### Mutex (sync.Mutex / sync.RWMutex) ``` For any sync.Mutex/RWMutex variable l and n < m: the n-th call to l.Unlock() happens-before the m-th call to l.Lock() returns. ``` То есть всё, что сделано в одной критической секции до `Unlock`, видно следующему держателю замка после `Lock`. ```go var mu sync.Mutex var balance int func deposit(n int) { mu.Lock() balance += n // защищено: видимость и атомарность секции mu.Unlock() } ``` Для `RWMutex`: вызов `RUnlock` происходит после соответствующего `RLock`, и `Unlock` пишущего happens-before последующих `RLock`/`Lock`. `TryLock`/`TryRLock` создают рёбра только при успехе. ### sync.Once ``` The completion of once.Do(f) (i.e. the return of f) happens-before the return of any call of once.Do(f). ``` `Do` гарантирует, что `f` выполнится ровно один раз, и **все** горутины, вызвавшие `Do`, после возврата увидят результаты `f`. Это правильный примитив для ленивой инициализации (в отличие от ручного double-checked locking, см. gotchas). ```go var once sync.Once var conn *Conn func Get() *Conn { once.Do(func() { conn = dial() }) // dial() ровно один раз return conn // гарантированно видим полностью инициализированный conn } ``` ### sync/atomic — sequential consistency (формализовано в 1.19) До Go 1.19 модель формально молчала о порядке памяти atomic-операций (хотя на практике они были SC). С 1.19 это закреплено. Цитата: > «The APIs in the sync/atomic package are collectively "atomic operations" that can be used to synchronize the execution of different goroutines. If the effect of an atomic operation A is observed by atomic operation B, then A is "synchronized before" B. All the atomic operations executed in a program behave as though executed in some sequentially consistent order.» То есть atomic-операции образуют единый глобальный SC-порядок, и «synchronized before» порождает happens-before. Это даёт самые сильные гарантии (sequential consistency), без отдельных acquire/release/relaxed режимов как в C++. С Go 1.19 появились **типизированные атомики**, которые предпочтительнее «свободных» функций `atomic.AddInt64` и т.д.: ```go import "sync/atomic" var ready atomic.Bool // типобезопасно, нельзя случайно прочитать не-атомарно var counter atomic.Int64 var cfg atomic.Pointer[Config] func publish(c *Config) { cfg.Store(c) // SC store ready.Store(true) } func consume() { if ready.Load() { // SC load use(cfg.Load()) // happens-before гарантирует видимость *Config целиком } } ``` Преимущество типов над функциями: значение нельзя случайно прочитать/записать неатомарно (нет доступа к «сырому» полю), нет проблемы выравнивания 64-битных значений на 32-битных платформах (для функций `atomic.AddInt64` требовалось ручное 8-байтное выравнивание первого слова структуры — типы это инкапсулируют). ### WaitGroup ``` A call to wg.Done() happens-before the return of any wg.Wait() call that it unblocks. ``` Всё, что горутина сделала до `Done()`, видно горутине, разблокированной `Wait()`. ```go var wg sync.WaitGroup results := make([]int, n) for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() results[i] = compute(i) // запись в разные индексы — без гонки }(i) } wg.Wait() // happens-after всех Done useAll(results) // гарантированно видны все записи ``` > Важно: `wg.Add` с положительным значением должен вызываться **до** старта горутины (или до соответствующего `Wait`), иначе гонка на самом счётчике. ### Дополнительные рёбра happens-before - **Запуск горутины**: оператор `go f()` (и вычисление его аргументов) happens-before начала выполнения `f`. Поэтому переданные аргументы видны новой горутине. - **Завершение горутины НЕ** создаёт happens-before автоматически — нельзя «дождаться» завершения горутины простым выходом из неё без синхронизации. - **`init`-функции пакета**: завершение всех `init` пакета happens-before старта `main`; импортируемый пакет инициализируется до импортирующего. - **runtime.Gosched / time.Sleep НЕ дают** гарантий видимости — это не синхронизация. Полагаться на sleep для упорядочивания памяти — ошибка. ### Sequential consistency для программ без гонок (DRF-SC) Главная практическая гарантия модели — **DRF-SC (Data-Race-Free ⇒ Sequentially Consistent)**: > Если программа свободна от data race, она исполняется так, как если бы все операции выполнялись в каком-то едином глобальном последовательном порядке, согласованном с program order каждой горутины. Это значит: **пишите код без гонок — и можете рассуждать о нём как о последовательной чередующейся (interleaved) программе.** Все хитрые правила выше нужны лишь чтобы установить отсутствие гонок и нужную видимость; дальше работает интуитивная SC-семантика. Оговорка из обновлённой модели: даже для программ с гонками Go даёт ограниченные гарантии «no out-of-thin-air values» и memory safety — race не приводит к чтению произвольного «выдуманного» значения и не ломает безопасность типов так, как UB в C++. Но конкретное наблюдаемое значение при гонке не определено. ## Подводные камни / gotchas ### 1. Публикация объекта через неатомарную переменную ```go // ОШИБКА: data race, нет happens-before между записью cfg и его чтением var cfg *Config // обычный указатель func writer() { cfg = loadConfig() } // запись func reader() { use(cfg) } // конкурентное чтение — ГОНКА ``` Даже если «указатель пишется атомарно на этой архитектуре» — модель этого не гарантирует, и нет ребра happens-before, упорядочивающего инициализацию `*Config` относительно чтения. Читатель может увидеть указатель, но **не увидеть записи в поля** структуры. Фикс — `atomic.Pointer[Config]` или мьютекс. ### 2. Double-checked locking без atomic ```go // ОШИБКА: классический сломанный DCL var instance *Service var mu sync.Mutex func Get() *Service { if instance == nil { // (A) чтение без синхронизации — ГОНКА с (B) mu.Lock() if instance == nil { instance = newService() // (B) запись под мьютексом } mu.Unlock() } return instance } ``` Проверка `(A)` вне замка конкурентна записи `(B)` — data race. Хуже того, читатель может увидеть ненулевой `instance`, но частично сконструированный объект (поля ещё не видны). Правильно — `sync.Once`, либо `atomic.Pointer`: ```go var instance atomic.Pointer[Service] var mu sync.Mutex func Get() *Service { if s := instance.Load(); s != nil { // atomic load — корректно return s } mu.Lock() defer mu.Unlock() if s := instance.Load(); s != nil { return s } s := newService() instance.Store(s) // atomic store — публикация return s } ``` (В идиоматичном Go просто используйте `sync.Once`.) ### 3. Конкурентный доступ к map — гонка и паника ```go m := map[string]int{} go func() { m["a"] = 1 }() // запись go func() { _ = m["a"] }() // конкурентное чтение/запись ``` Карты в Go **не** потокобезопасны. Конкурентная запись (или запись+чтение) — data race, и рантайм имеет встроенный детектор concurrent map writes, который **аварийно завершает программу** (`fatal error: concurrent map writes`) — это не recoverable panic. Фикс: `sync.Mutex`/`sync.RWMutex` вокруг карты или `sync.Map` для подходящих паттернов. ### 4. Опора на time.Sleep / порядок печати как на синхронизацию ```go go func() { data = 1 }() time.Sleep(time.Millisecond) // НЕ гарантирует видимость data println(data) // всё ещё гонка ``` Sleep не создаёт happens-before. Гонка остаётся гонкой, даже если «на практике обычно работает». ### 5. Цикл переменной в горутине (до Go 1.22) ```go for _, v := range items { go func() { use(v) }() // до Go 1.22: все горутины делят одну v — гонка + неверные данные } ``` До Go 1.22 переменная цикла переиспользовалась — классическая ловушка (захват по ссылке + гонка). С Go 1.22 переменная цикла на каждой итерации новая. Но для совместимости и явности часто всё равно передают параметром: `go func(v T){...}(v)`. ### 6. Передача аргументов в go vs замыкание `go f(x)` копирует `x` в момент запуска (есть happens-before на вычисление аргументов). Захват по замыканию (`go func(){ use(x) }()`) читает `x` уже в новой горутине — если `x` потом меняется в исходной горутине без синхронизации, это гонка. ## Вопросы на собеседовании **В:** Что именно гарантирует Go memory model, а что нет? **О:** Она задаёт условия, при которых чтение переменной в одной горутине гарантированно видит запись из другой — через отношение happens-before. Внутри одной горутины код «как написан» (single-goroutine SC). Между горутинами никаких гарантий видимости нет, пока вы явно не установили happens-before через примитив синхронизации (channel, mutex, Once, atomic, WaitGroup). Модель НЕ гарантирует ничего о порядке/видимости для конкурентных не-atomic доступов — это data race с неопределённым поведением (с оговорками про memory safety и no-out-of-thin-air в редакции 1.19). **В:** Сформулируй отношение happens-before и теорему о видимости чтения. **О:** Happens-before — частичный порядок на memory operations, образованный program order внутри горутины, транзитивностью и synchronization edges от примитивов. Чтение r видит запись w, если w happens-before r и нет промежуточной записи w' с w hb w' hb r. Если r и w конкурентны (нет hb ни в одну сторону) — это гонка, и r может увидеть либо не увидеть w. **В:** Какие happens-before-гарантии дают каналы? В чём разница буферизованного и небуферизованного? **О:** (1) send happens-before завершения соответствующего receive — так передаются данные. (2) close happens-before receive, который вернул zero из-за закрытия. (3) Для unbuffered: receive happens-before завершения send (двустороннее рукопожатие). (4) Для буферизованного capacity C: k-й receive happens-before завершения (k+C)-го send. Разница принципиальна: на unbuffered можно полагаться, что отправитель «дождался» получателя; на буферизованном такой обратной гарантии нет, аналогичный код может увидеть незаписанное значение. **В:** Чем плох double-checked locking без atomic и как чинить? **О:** Проверка `instance == nil` вне замка конкурентна записи под замком — data race. Даже если читатель увидит ненулевой указатель, инициализация полей объекта может быть не видна (нет happens-before, упорядочивающего конструирование относительно чтения). Чинить: `sync.Once` (идиоматично) или `atomic.Pointer[T]` с atomic Load/Store, которые дают SC-порядок и корректную публикацию. **В:** Почему публикация структуры через обычный указатель — гонка, даже если запись указателя «атомарна» на железе? **О:** Модель не обещает атомарности обычной записи указателя, и, что важнее, нет ребра happens-before между записью полей структуры и их чтением в другой горутине. Без синхронизации компилятор/CPU вправе переупорядочить инициализацию полей и публикацию указателя, и читатель увидит указатель раньше, чем содержимое. Нужен atomic.Pointer (SC) или мьютекс, чтобы установить happens-before и гарантировать видимость всей структуры. **В:** Что нового в sync/atomic с Go 1.19? **О:** Два изменения. Формально: модель закрепила, что все atomic-операции ведут себя так, будто выполнены в едином sequentially consistent порядке, и «synchronized before» порождает happens-before (раньше спецификация это формально не описывала). Практически: появились типизированные атомики (`atomic.Bool`, `atomic.Int64`, `atomic.Pointer[T]` и т.д.), которые безопаснее функций — нельзя случайно обратиться к значению неатомарно и нет проблемы 8-байтного выравнивания на 32-битных платформах. **В:** Что такое DRF-SC и почему это главное практическое следствие модели? **О:** Data-Race-Free implies Sequential Consistency: если в программе нет ни одной гонки, она исполняется так, будто все операции идут в едином глобальном порядке, согласованном с program order каждой горутины. Это позволяет рассуждать о корректной конкурентной программе как о простом чередовании последовательных шагов, не вникая в переупорядочивания компилятора/CPU. Весь смысл правил happens-before — обеспечить отсутствие гонок, после чего работает интуитивная SC-семантика. **В:** Создаёт ли завершение горутины happens-before? А `go f()`? А time.Sleep? **О:** Запуск `go f()` создаёт happens-before: вычисление аргументов и сам оператор go happens-before начала f, поэтому аргументы видны новой горутине. Завершение горутины НЕ создаёт happens-before автоматически — чтобы безопасно увидеть её результаты, нужна явная синхронизация (channel/WaitGroup/mutex). time.Sleep, Gosched и порядок вывода никаких гарантий памяти не дают — это не примитивы синхронизации, и опираться на них для упорядочивания нельзя. **В:** Почему конкурентная запись в map валит программу, а не просто даёт гонку? **О:** Карты не потокобезопасны, и конкурентная запись повредила бы внутреннюю структуру (бакеты, при росте — эвакуацию). Рантайм имеет дешёвый встроенный детектор (флаг hashWriting), который при обнаружении конкурентной записи вызывает fatal error «concurrent map writes» — это не recoverable panic, программа аварийно завершается, чтобы не продолжать с повреждённой памятью. Решение: мьютекс или sync.Map. **В:** Как практически находить нарушения модели памяти? **О:** Запускать с race detector: `go test -race`, `go run -race`, `go build -race`. Это ThreadSanitizer-инструментализация, которая в рантайме строит happens-before-граф и ловит конкурентные не-синхронизированные доступы. Он находит только гонки, реально случившиеся в данном прогоне, поэтому важно гонять под реалистичной нагрузкой и в CI. Накладные расходы значительны (память/CPU), поэтому в проде обычно не включают. ## На что копают на senior+ - **Unbuffered vs buffered happens-before**: умение объяснить правило «receive на unbuffered hb завершения send» и привести пример, где замена unbuffered на buffered ломает корректность. - **Точная формулировка DRF-SC** и оговорки редакции 2022/1.19: что даёт модель для программ С гонками (memory safety, no out-of-thin-air) и чего не даёт (определённого значения). - **sync/atomic как SC, без acquire/release**: понимание, что Go не предоставляет relaxed/acq-rel-режимов как C++/Java, все atomic — sequentially consistent, и почему это сознательный выбор (простота рассуждений ценой иногда лишних барьеров). - **Корректная публикация**: глубокое понимание, что atomic.Pointer.Store публикует не только указатель, но и (через happens-before) всё, что записано до Store, делая видимым содержимое структуры читателю после Load. - **Почему sync.Once правильнее ручного DCL**, и как Once гарантирует видимость результата f всем вызывающим (completion of f hb return of any Do). - **Аргументы go vs захват замыканием** и тонкость с переменной цикла до/после Go 1.22. - **Ограничения race detector**: он динамический (не статический), ловит только наблюдённые в прогоне гонки, отсюда — необходимость нагрузочного покрытия; понимание, что «прошло под -race» ≠ «гонок нет». - **Цитирование источника**: знание, что есть официальный документ go.dev/ref/mem, переписанный в 2022, и его центральный совет «Don't be clever» — синхронизируйтесь явно, а не полагайтесь на тонкости.