Go Interview Refresh: Language, Concurrency, and Sync

A focused refresher course to prepare for a Go developer interview, emphasizing core language knowledge and practical concurrency patterns. You will review goroutines, channels, and the sync package, and learn how to reason about common pitfalls and interview-style tasks.

1. Go Fundamentals: Types, Interfaces, and Error Handling

Go Fundamentals: Types, Interfaces, and Error Handling

This course is about being interview-ready in Go: not just “I’ve used it”, but “I understand why it works this way”. This first article refreshes the foundations you’ll be evaluated on constantly:

  • Types and zero values (how data behaves by default)
  • Methods and interfaces (how Go models polymorphism)
  • Error handling (how production Go code communicates failure)
  • These topics also set you up for the next parts of the course (goroutines, channels, and sync), because concurrency bugs often start as type, interface, or error misuse.

    Types and zero values

    Go is a statically typed language: every value has a type known at compile time (even when inferred).

    Basic vs composite types

    Common basic types:

  • bool
  • string
  • int, int64, uint, uintptr
  • float64
  • complex128
  • Common composite types:

  • Arrays: [N]T
  • Slices: []T
  • Maps: map[K]V
  • Structs: struct{ ... }
  • Pointers: *T
  • Functions: func(...) ...
  • Interfaces: interface{ ... }
  • The Go spec is the most precise reference if an interviewer pushes you on definitions: The Go Programming Language Specification.

    Zero values

    Every type has a zero value: what you get when you declare a variable without initializing it.

  • Numbers: 0
  • bool: false
  • string: ""
  • Pointers, slices, maps, functions, channels, interfaces: nil
  • Struct: each field is its own zero value
  • Array: each element is its own zero value
  • Key detail: nil is not “empty”; it’s “not initialized”. Some operations work on nil values, some do not.

    Value semantics vs “reference-like” types

    In Go, assignment copies the value. But some values contain internal pointers to shared backing storage.

  • Arrays are values: copying [3]int copies all elements.
  • Slices are small headers (pointer, length, capacity): copying a slice copies the header, but the header can still point to the same backing array.
  • Maps are descriptors referencing runtime-managed storage: copying a map value makes two variables refer to the same map data.
  • This matters a lot later with goroutines: if two goroutines share a map or a slice backing array, you must synchronize access.

    new vs make

    These are commonly tested.

  • new(T) allocates a zero value of type T and returns *T.
  • make(T, ...) initializes runtime-backed types: slice, map, chan.
  • Rule of thumb: if you need a usable map/channel/slice, you want make.

    Variables, type inference, and constants

    Declarations and inference

  • var x int = 10 is explicit.
  • var x = 10 is inferred.
  • x := 10 is short declaration (only inside functions).
  • Be careful in interviews with := and shadowing:

    Untyped constants

    Go constants can be untyped until context requires a type.

    This is one reason Go constant expressions feel flexible.

    Methods and receiver semantics

    Methods are functions with a receiver:

    Value receiver vs pointer receiver

  • Use a pointer receiver when the method must mutate the receiver, or when copying would be expensive.
  • Use a value receiver when the method doesn’t mutate and the type is small and clearly copyable.
  • Method sets (core interview topic)

    The method set determines which interfaces a type implements.

  • The method set of T includes methods with receiver T.
  • The method set of T includes methods with receiver T and T.
  • That implies:

  • If an interface requires a method with a pointer receiver, only *T implements it.
  • !Diagram showing how method sets differ for T vs *T

    Interfaces

    An interface is a set of method signatures. A type implements an interface implicitly by having the required methods.

    Effective, idiomatic interfaces are usually small (often 1–2 methods). See: Effective Go.

    Defining and using interfaces

    Reference: package io.

    Interface composition

    Interfaces can embed other interfaces:

    This is a common way to express capabilities.

    The empty interface and any

  • interface{} can hold any value.
  • any is an alias for interface{}.
  • Prefer precise types and interfaces over any in production code. In interviews, you’ll often be asked how to safely recover a concrete type from any.

    Type assertions and type switches

    Type assertion:

    Type switch:

    The nil interface pitfall

    An interface value is conceptually a pair:

  • a dynamic type
  • a dynamic value
  • The interface is nil only when both are nil.

    This shows up a lot with error returns.

    Error handling

    In Go, errors are values. By convention, a function that can fail returns (T, error) (or just error).

    The built-in error is an interface:

    Reference: package errors.

    Creating errors

  • errors.New("message")
  • fmt.Errorf("formatted %s", x)
  • Wrapping errors (and why it matters)

    Wrapping preserves context while keeping the original error discoverable.

    %w is special: it marks the wrapped error so it can be inspected later.

    Reference: package fmt.

    Checking errors: errors.Is and errors.As

  • Use errors.Is(err, target) to check whether any error in the wrapping chain matches target.
  • Use errors.As(err, &targetType) to extract a specific concrete error type from the chain.
  • The errors package documents the wrapping and inspection rules: package errors.

    Sentinel errors vs custom error types

    Two common patterns:

  • Sentinel error: a package-level var (like io.EOF) used for equality/errors.Is checks.
  • Custom error type: a struct type that carries structured data.
  • Prefer custom types when callers need to branch on details, not on a single constant.

    panic vs returning errors

  • Return errors for expected failure modes (I/O errors, invalid input, timeouts).
  • Use panic for programmer mistakes or impossible states.
  • panic can be recovered with defer + recover, but this is not a replacement for normal error handling.

    Reference: Defer, Panic, and Recover.

    Practical guidelines you can say out loud in an interview

  • Use zero values to make APIs easy to use (a struct’s zero value should be meaningful when possible).
  • Avoid any in public APIs unless you truly need it.
  • Keep interfaces small and define them where they are consumed, not where they are implemented.
  • Wrap errors with context (fmt.Errorf("...: %w", err)) and inspect with errors.Is / errors.As.
  • Be explicit about pointer vs value receivers and understand how method sets affect interface satisfaction.
  • In the next articles, these fundamentals will reappear constantly: channels are typed, sync relies on pointer semantics, and context cancellation/timeout patterns are built around interfaces and error propagation.

    2. Memory Model Essentials: Pointers, Escape Analysis, and GC

    Memory Model Essentials: Pointers, Escape Analysis, and GC

    Эта статья продолжает фундамент из предыдущей темы (типы, интерфейсы, ошибки) и подводит к тому, что почти всегда всплывает на собеседованиях по Go, особенно перед темами про goroutines, каналы и sync: как значения живут в памяти, когда и почему происходят аллокации, и что на самом деле гарантируется между потоками исполнения.

    Если кратко, интервьюер часто хочет услышать, что вы понимаете три вещи:

  • Указатели и владение временем жизни: что именно вы “делите” между частями программы.
  • Escape analysis: почему “взял адрес” не означает “выделил на куче”, и как компилятор принимает решение.
  • GC: почему аллокации — не бесплатны, как сборщик мусора влияет на задержки и throughput.
  • Память в Go: стек, куча и почему “я сам решу где хранить” — не про Go

    В Go вы обычно не управляете размещением “вручную” (как в C/C++). Вы пишете код в терминах значений и указателей, а компилятор решает, может ли объект жить на стеке, или он должен жить в куче.

    Стек и куча в одном абзаце

  • Стек — память, привязанная ко времени жизни вызова функции (и цепочки вызовов). Очень дёшево выделять и освобождать.
  • Куча (heap) — память, где объекты могут жить дольше, чем вызов функции; её освобождает GC.
  • Ключевая мысль: в Go вы не говорите “положи это на стек”. Вы пишете код так, чтобы объект не убегал (не escape’ился) за пределы своего времени жизни, и тогда компилятор часто сможет оставить его на стеке.

    !Схема: когда локальная переменная остаётся на стеке, а когда "убегает" в кучу (escape).

    Указатели в Go: что именно вы разделяете

    Указатель *T хранит адрес значения типа T. Это базовый инструмент, который напрямую связан с тем, будет ли объект разделяться между частями программы.

    Что важно проговаривать на собеседовании

  • Передача значения обычно означает копирование.
  • Передача указателя означает, что вы можете менять одно и то же значение из разных мест.
  • Reference-like” типы (slice, map, chan, func) сами по себе маленькие дескрипторы, которые указывают на внутренние структуры. Даже если вы передали их “по значению”, вы часто всё равно разделяете общую память.
  • Пример: копирование slice копирует заголовок, но может ссылаться на тот же backing array.

    Для будущих тем курса (goroutines/каналы/sync) это критично: “мы передали слайс” не означает “каждый получил свою копию данных”.

    Escape analysis: почему &x не всегда означает кучу

    Escape analysis — это анализ компилятором времени жизни данных: может ли значение быть уничтожено при выходе из функции, или оно будет использоваться позже.

    Если компилятор видит, что значение должно жить дольше, чем текущий стековый фрейм, оно escape’ится в кучу.

    Типичные причины escape

  • Вы возвращаете адрес локальной переменной
  • Значение захватывается замыканием (closure)
  • Значение уходит в interface и компилятор не может доказать, что оно не должно жить дольше
  • В реальности детали зависят от конкретного кода, версии компилятора и того, может ли он доказать время жизни. Важно понимать идею: интерфейсы могут приводить к дополнительным аллокациям, когда значение нужно “упаковать”.

  • Вы передаёте указатель туда, где он будет жить дольше текущей функции
  • Например, сохраняете в структуре, глобальной переменной или отправляете в горутину (через замыкание).

    Важный анти-миф

  • new(T) не означает “обязательно куча”. Это всего лишь “получи адрес нулевого T”. Если объект не escape’ится, компилятор может разместить его на стеке.
  • Точно так же make([]T, ...) или make(chan ...) не является обещанием “это в куче” на уровне спецификации; но на практике многие runtime-структуры живут в куче, потому что их время жизни часто шире текущей функции.
  • Как смотреть escape analysis

    В Go принято смотреть решения компилятора через флаги компиляции:

    Флаги относятся к компилятору, и их удобно запускать для точечной диагностики. Документация по команде go: cmd/go.

    Что важно: результаты escape analysis — это не “ошибка” и не “предупреждение”, а объяснение, почему компилятор принял решение об аллокации.

    GC в Go: базовая модель, стоимость аллокаций и что спрашивают

    В Go память в куче освобождает garbage collector. На практике собеседования редко требуют внутренних деталей “до байта”, но часто требуют корректной ментальной модели: аллокации стоят денег, GC работает параллельно, но имеет паузы, и излишние аллокации увеличивают нагрузку.

    Официальный обзор GC: A Guide to the Go Garbage Collector.

    Что такое “сборка мусора” в терминах программы

  • В куче живут объекты.
  • Есть корни (roots): например, глобальные переменные и то, что достижимо из стеков горутин.
  • Всё, что достижимо из корней по ссылкам, считается “живым”. Остальное можно освободить.
  • Отсюда полезное практическое правило: если вы держите ссылку (указатель, slice, map, chan) в долгоживущей структуре, то связанные с ней объекты будут оставаться живыми.

    Почему аллокации влияют на latency

  • Чем больше объектов вы аллоцируете, тем больше работы у GC.
  • Даже при конкурентном GC существуют короткие фазы stop-the-world.
  • Это особенно заметно в высоконагруженных сервисах и в горячих циклах.
  • GOGC и контроль агрессивности GC

    Переменная окружения GOGC управляет тем, насколько куча может вырасти между циклами GC.

  • GOGC=100 (значение по умолчанию) означает: когда объём кучи примерно удвоится относительно “после последнего GC”, пора собирать снова.
  • Меньше GOGC — чаще GC (обычно ниже потребление памяти, но выше CPU на GC).
  • Больше GOGC — реже GC (обычно выше потребление памяти, но ниже CPU на GC).
  • Go memory model: что гарантируется между горутинами

    Даже идеальное понимание стека/кучи не спасает от ошибок конкурентности, если не понимать модель памяти: когда запись в одной горутине “становится видимой” в другой.

    Официальный документ: The Go Memory Model.

    Ключевое слово: happens-before

    Go гарантирует видимость записей между горутинами только при наличии отношения happens-before, которое создаётся синхронизацией.

    Основные источники happens-before (на уровне, полезном для интервью):

  • Mutex: Unlock в одной горутине happens-before успешного Lock в другой (для того же мьютекса).
  • Channels: отправка в канал happens-before соответствующего получения этого значения.
  • WaitGroup: завершение горутин через Done и ожидание через Wait используется как барьер завершения.
  • atomic операции дают упорядочивание (в зависимости от операции), и применяются для очень узких случаев.
  • Без синхронизации чтение/запись общей памяти — это data race, и поведение программы становится недостоверным: вы не можете рассуждать “ну запись же была раньше по времени”.

    Практические выводы, которые хорошо звучат на собеседовании

  • Адрес (&x) не равен куче: размещение решает компилятор через escape analysis.
  • Меньше аллокаций — меньше нагрузки на GC: оптимизация часто начинается с устранения лишних временных объектов.
  • Передача slice/map/chan “по значению” не означает копирование данных: вы часто разделяете одну и ту же память.
  • В конкурентности важна не “очерёдность в коде”, а happens-before: видимость обеспечивают каналы, мьютексы, WaitGroup, atomic.
  • Эта статья — мост к следующей части курса: когда вы начнёте активно использовать goroutines и sync, вопросы “что я разделяю?”, “как это синхронизировано?” и “не создал ли я лишние аллокации?” станут постоянными.

    3. Goroutines and Scheduling: Concurrency Basics

    Goroutines and Scheduling: Concurrency Basics

    Эта статья — мост от тем про типы/интерфейсы/ошибки и модель памяти/escape analysis/GC к тому, что почти всегда проверяют на Go-интервью: умение писать корректный конкурентный код и понимать, что реально происходит при запуске goroutine.

    Ключевая мысль: goroutine — это дешёвая единица конкурентности, но корректность и производительность зависят от того, как вы ограничиваете параллелизм, как синхронизируете доступ к данным и как избегаете утечек.

    Concurrency и parallelism

  • Concurrency (конкурентность): программа умеет переключаться между несколькими задачами и продвигать их выполнение.
  • Parallelism (параллелизм): задачи реально выполняются одновременно на нескольких ядрах.
  • В Go конкурентность выражается через goroutine и каналы/синхронизацию, а параллелизм ограничивается настройкой рантайма и ресурсами машины.

    Полезный контекст для интервью: доклад/эссе Concurrency is not parallelism.

    Что такое goroutine

    Goroutine — это функция, запущенная конкурентно с помощью go.

    Формально это описано в спецификации: Go specification: Go statements.

    Почему goroutine “дешёвые”, но не бесплатные

  • У goroutine есть стек, который может расти и сжиматься (в отличие от фиксированных потоков ОС).
  • Планированием занимается рантайм Go, что уменьшает стоимость создания по сравнению с потоками ОС.
  • Но goroutine всё равно расходуют память (стек, метаданные), а огромное число заблокированных goroutine часто означает утечку по логике.
  • Связь с предыдущей статьёй: чем больше “случайных” аллокаций и удержания объектов (например, через замыкания), тем больше работа для GC и тем хуже задержки.

    Планировщик Go: базовая модель

    На интервью часто ждут ментальную модель, а не внутренности на уровне исходников рантайма.

    Модель G-M-P (очень коротко)

  • G (goroutine): то, что вы создаёте через go.
  • M (machine): поток ОС, на котором выполняется Go-код.
  • P (processor): логический “токен” для исполнения Go-кода; именно P ограничивает, сколько Go-кода может исполняться параллельно.
  • Количество P задаётся GOMAXPROCS.

    !Схема, объясняющая как рантайм Go планирует goroutine через модель G-M-P

    GOMAXPROCS и что он реально ограничивает

    GOMAXPROCS — это верхняя граница параллельного выполнения Go-кода (по числу P), а не число goroutine.

  • Если GOMAXPROCS = 1, goroutine будут конкурентно выполняться на одном ядре (параллелизма нет, но конкурентность есть).
  • Если GOMAXPROCS = N, рантайм может исполнять до N goroutine одновременно на разных ядрах (если есть работа).
  • Справка: package runtime: GOMAXPROCS.

    Блокировки, syscalls и почему “у меня всё зависло”

    Goroutine может перестать выполняться, когда:

  • Она ждёт Mutex/RWMutex.
  • Она ждёт чтение/запись в канал.
  • Она делает блокирующий ввод-вывод (syscall).
  • Она ждёт time.Sleep, WaitGroup.Wait и т.д.
  • Для интервью важно говорить корректно:

  • “Goroutine блокируется” — это про ожидание события.
  • “Поток ОС блокируется на syscall” — это другой уровень; рантайм старается не “заморозить” весь прогресс программы из-за одного блокирующего потока.
  • Goroutine и модель памяти: видимость данных

    Из прошлой статьи: без happens-before между горутинами чтение/запись общей памяти — это data race. Планировщик может переключать выполнение так, что “по времени” всё выглядит последовательно, но гарантий нет.

  • Мьютексы, каналы, WaitGroup, atomic — это не только “защита”, но и гарантия видимости.
  • Официальная опора: The Go Memory Model.

    Практический вывод для интервью: если вы запускаете goroutine и она пишет в общие переменные, то “потом прочитаю” корректно только при явной синхронизации.

    Частые ошибки с goroutine (то, что любят спрашивать)

    Захват переменной цикла в замыкании

    Классический баг:

    Исправления:

    1) Передавать значение параметром:

    2) Или создавать новую переменную в теле цикла:

    Связь с памятью/escape: замыкания часто приводят к escape переменных, потому что значение должно жить дольше текущей итерации/функции.

    Утечки goroutine

    Утечка — это когда goroutine “навсегда” зависла в ожидании и никто её больше не разблокирует. Частые причины:

  • Ожидание чтения из канала, в который никто больше не пишет.
  • Ожидание записи в канал, из которого никто не читает.
  • Ожидание “сигнала завершения”, который никогда не приходит.
  • Интервью-фраза: “Каждая goroutine должна иметь понятный путь завершения”.

    Неконтролируемый fan-out

    Запускать goroutine “на каждую задачу” без ограничений опасно:

  • Можно создать десятки/сотни тысяч goroutine.
  • Резко вырастет потребление памяти.
  • Увеличится давление на GC.
  • Типичный подход: ограничивать параллелизм через worker pool (в следующей теме про каналы это будет естественным продолжением).

    Полезные инструменты рантайма (которые могут спросить)

    runtime.Gosched

    runtime.Gosched() добровольно отдаёт управление планировщику, позволяя другим goroutine выполниться.

    Справка: package runtime: Gosched.

    На интервью важно сказать: в “нормальном” коде это редкий инструмент; чаще проблема решается правильной синхронизацией/архитектурой, а не ручным “уступанием”.

    runtime.LockOSThread

    Иногда нужно привязать goroutine к конкретному потоку ОС (например, из-за требований C-библиотек, TLS или GUI-потоков).

    Справка: package runtime: LockOSThread.

    Важно: это продвинутый инструмент, который уменьшает свободу планировщика и может ухудшить производительность.

    Как проверять конкурентный код

    Race detector

    Для подготовки к интервью полезно уметь запускать гонки:

    Официальное описание: Data Race Detector.

    На интервью хорошая формулировка: “Я не рассуждаю о корректности под нагрузкой без -race и понятной синхронизации (happens-before)”.

    Что проговорить на собеседовании (короткий чек-лист)

  • Goroutine — дёшево, но утечки goroutine и неконтролируемый fan-out убивают сервис.
  • Параллелизм Go-кода ограничен GOMAXPROCS (через P), а не количеством goroutine.
  • Без синхронизации нет happens-before, значит нет гарантий видимости и есть data race.
  • Замыкания + цикл — частая ловушка; исправление через параметр или v := v.
  • Дальше курс логично продолжать темами про каналы и пакет sync: именно они дают основные строительные блоки для структурированной конкурентности в Go.

    4. Channels in Depth: Patterns, Select, and Cancellation

    Channels in Depth: Patterns, Select, and Cancellation

    Эта статья продолжает линию курса: после goroutines и планировщика мы переходим к главному строительному блоку конкурентности в Go — каналам. На собеседовании важно не просто знать синтаксис chan, а уметь объяснить:

  • почему канал — это одновременно синхронизация и передача данных
  • как устроены блокировки (и как они приводят к утечкам goroutine)
  • как select реализует мультиплексирование событий
  • как делать отмену, таймауты и корректное завершение пайплайнов
  • Ключевая связка с прошлыми темами:

  • из статьи про модель памяти вы уже знаете про happens-before: операции с каналами создают гарантии видимости
  • из статьи про goroutines вы уже знаете, что “каждая goroutine должна иметь путь завершения” — каналы и отмена как раз про это
  • Канал как тип и как синхронизация

    Канал — типизированный провод для передачи значений между goroutine.

  • chan T — двунаправленный канал
  • chan<- T — только отправка
  • <-chan T — только получение
  • Спецификация: Go specification: Channel types.

    Канал важен не только как “очередь”, но и как точка синхронизации.

  • Отправка ch <- v happens-before соответствующего получения x := <-ch для того же значения.
  • Это означает: если goroutine A записала какие-то данные, а потом отправила в канал сигнал/значение, то goroutine B, получив это значение, увидит эти записи корректно (при условии, что данные опубликованы через эту синхронизацию).

    Источник формулировок: The Go Memory Model.

    Буферизированные и небуферизированные каналы

    Небуферизированный канал

    make(chan T) создаёт небуферизированный канал.

  • Отправка блокируется, пока кто-то не начнёт получать
  • Получение блокируется, пока кто-то не начнёт отправлять
  • Это похоже на рандеву: обе стороны встречаются в одной точке.

    Буферизированный канал

    make(chan T, n) создаёт буфер вместимостью n.

  • Отправка блокируется, только когда буфер заполнен
  • Получение блокируется, только когда буфер пуст
  • На интервью важно проговорить: буфер не делает код “параллельным” автоматически, он лишь меняет точки блокировки и может скрыть (или создать) проблемы с backpressure.

    !Сравнение блокировок для unbuffered и buffered каналов

    Операции с каналом: отправка, получение, закрытие

    Получение: одно значение и “ok-форма”

    Получение может быть двух видов:

  • ok == true: значение получено от отправителя
  • ok == false: канал закрыт и значения в буфере закончились
  • Важная деталь: чтение из закрытого канала не блокируется и возвращает zero value типа T и ok == false.

    Закрытие канала

    Закрытие: close(ch).

  • Закрывать должен отправитель (владелец “потока значений”)
  • Закрытие — это сигнал “значений больше не будет”, а не “уничтожить канал”
  • Критические правила (часто спрашивают):

  • Отправка в закрытый канал вызывает panic
  • Закрывать канал второй раз вызывает panic
  • Закрытие не “выбрасывает” уже отправленные значения: получатель может дочитать буфер
  • range по каналу

    Идиоматичное потребление “потока”:

    Цикл завершится, когда канал закрыт и все значения получены.

    nil канал как инструмент (и как ловушка)

    Нулевое значение канала — nil.

  • Чтение из nil-канала блокируется навсегда
  • Запись в nil-канал блокируется навсегда
  • close(nil) вызывает panic
  • Это может быть ловушкой (утечка goroutine), но иногда это полезный приём в select: можно “выключить” кейс, присвоив каналу nil.

    select: мультиплексирование и управление блокировками

    select позволяет ждать несколько операций с каналами одновременно.

    Ключевые свойства:

  • Если готовы несколько кейсов, выбирается один псевдослучайно (справедливо в среднем)
  • Если нет готовых кейсов и нет default, select блокируется
  • Если есть default, select никогда не блокируется
  • Спецификация: Go specification: Select statements.

    default и активное ожидание

    default часто используют неправильно, превращая ожидание в busy loop.

    Плохо:

    Хорошо: если нужен неблокирующий опрос, добавляйте паузу или архитектурно уходите в блокирующее ожидание.

    Таймауты и дедлайны: time.After и time.NewTimer

    Быстрый таймаут через time.After

  • time.After(d) возвращает канал, который получит событие через d
  • Удобно для простых мест
  • Справка: package time: After.

    Когда лучше time.NewTimer

    В циклах часто лучше использовать NewTimer, чтобы контролировать жизненный цикл таймера и не плодить лишние объекты.

    Справка: package time: NewTimer.

    На интервью достаточно сказать: time.After прост, но в горячих циклах может быть менее подходящим; NewTimer даёт явное управление и Stop.

    Отмена (cancellation): context и done-каналы

    Тема отмены — одна из самых частых причин утечек goroutine: кто-то ждёт чтение/запись, а отмена запроса уже произошла.

    Идиоматичный способ: context.Context

    context даёт стандартный канал отмены: ctx.Done().

  • Done() закрывается при отмене/таймауте
  • Err() возвращает причину (context.Canceled или context.DeadlineExceeded)
  • Справка: package context и статья Go blog: Context.

    Свой done-канал

    Иногда делают явный канал:

    Тип struct{} выбран, потому что он не несёт данных и имеет нулевой размер.

    Правило: done обычно закрывают, а не “посылают одно значение”, потому что закрытие широковещательно — все получатели сразу разблокируются.

    Базовые паттерны с каналами, которые любят на интервью

    Pipeline: стадийная обработка

    Pipeline — это несколько стадий, соединённых каналами: каждая стадия читает из входного канала и пишет в выходной.

    Правила корректности:

  • каждая стадия должна корректно завершаться при закрытии входа
  • каждая стадия должна уважать отмену (ctx.Done() или done)
  • выходной канал обычно закрывает именно стадия-писатель
  • См. классический материал: Go blog: Pipelines and cancellation.

    !Пайплайн и распространение отмены

    Fan-out / fan-in

  • Fan-out: один входной канал распределяется на несколько worker goroutine
  • Fan-in: несколько источников объединяются в один канал
  • Что важно на интервью:

  • fan-out почти всегда требует ограничения параллелизма (иначе неконтролируемый fan-out)
  • fan-in требует аккуратного закрытия результата: обычно через WaitGroup (мы детально разберём это в статье про sync), либо через отдельную goroutine-координатор
  • Worker pool (ограничение параллелизма)

    Идея: фиксированное число воркеров читают задачи из канала jobs.

    Это одновременно:

  • контроль параллелизма
  • backpressure: если jobs не читается, отправители блокируются
  • На интервью полезная формулировка: “канал задач плюс N воркеров — простой и предсказуемый способ ограничить параллелизм без запуска goroutine на каждую задачу”.

    Semaphore на буферизированном канале

    Частый приём: ограничить количество одновременных операций.

  • acquire блокируется, когда лимит исчерпан
  • release освобождает слот
  • Важно: это не заменяет Mutex для защиты данных, это ограничитель параллелизма.

    Типовые баги и как их объяснять

    Deadlock из-за несоответствия отправителей и получателей

    Причины:

  • никто не читает из канала, а кто-то пишет (и блокируется)
  • никто не пишет, а кто-то читает (и блокируется)
  • Инструмент:

  • рантайм может завершить программу с fatal error: all goroutines are asleep - deadlock!
  • Утечка goroutine из-за отсутствия отмены

    Частый сценарий:

  • worker ждёт чтение из jobs
  • владелец запроса уже ушёл (ошибка/таймаут), но jobs никто не закрывает и done не сигналит
  • Лечение:

  • использовать context и select с ctx.Done()
  • аккуратно проектировать владение каналами: кто создаёт, кто закрывает, кто и когда прекращает чтение
  • Закрытие канала “не тем”

    Если канал используется несколькими отправителями, закрывать его “из одного отправителя” опасно: остальные могут попытаться отправить и получить panic.

    Подходы:

  • один владелец отправки (централизовать отправку)
  • закрывать канал координатором, который точно знает, что отправители завершились (обычно через sync.WaitGroup)
  • Что проговорить на собеседовании

  • Каналы дают не только передачу значений, но и синхронизацию: отправка happens-before получением.
  • Unbuffered — это рандеву, buffered — очередь ограниченной ёмкости; буфер меняет точки блокировки и backpressure.
  • close — сигнал “значений больше не будет”; отправка в закрытый канал — panic.
  • select — основной инструмент для конкурентного ожидания, таймаутов и отмены.
  • Отмена должна быть частью дизайна: ctx.Done() или done-канал, иначе легко получить утечки goroutine.
  • В следующей части курса пакет sync расширит этот набор инструментов: Mutex, RWMutex, WaitGroup, Cond, Once, Pool и их связь с моделью памяти и каналами.

    5. sync Package: Mutexes, RWMutex, WaitGroup, Once, Cond, Pool

    sync Package: Mutexes, RWMutex, WaitGroup, Once, Cond, Pool

    This article completes the core interview arc that started with Go fundamentals and the memory model, continued with goroutines and channels, and now lands on the second pillar of concurrency in Go: the sync package.

    If channels are about communicating by passing data, sync is about protecting shared state and coordinating execution. Interviewers expect you to know both, and to clearly explain:

  • when you should use a mutex vs a channel
  • what correctness guarantees you get (especially happens-before)
  • common deadlocks, races, and goroutine leaks
  • References you can cite confidently:

  • sync package documentation
  • The Go Memory Model
  • Concurrency toolbox: channels vs sync

    A practical interview framing:

  • Use channels when you want to structure your program as a pipeline / job queue / message passing system.
  • Use sync primitives when multiple goroutines must access or coordinate around shared memory.
  • Both create happens-before relationships, but in different ways.

  • With channels: send happens-before the corresponding receive.
  • With mutexes: Unlock happens-before a later successful Lock on the same mutex.
  • This connection to the memory model is often the real point of the question.

    A quick map of the sync primitives

    | Primitive | Primary purpose | Typical use | Common pitfall | |---|---|---|---| | Mutex | mutual exclusion | protect shared state | forgetting Unlock, locking order deadlocks | | RWMutex | shared reads, exclusive writes | read-heavy maps/caches | reader starvation, copying mutex value | | WaitGroup | wait for goroutines to finish | fan-out worker completion | calling Add concurrently with Wait, leaks | | Once | run exactly once | lazy init | init function blocking forever | | Cond | condition variable | complex coordination with predicates | forgetting to loop on condition | | Pool | reuse temporary objects | reduce allocations/GC | assuming pool is a cache / stable storage |

    sync.Mutex

    A Mutex protects a critical section so only one goroutine can access it at a time.

    The two rules interviewers look for

  • The mutex must not be copied after first use.
  • Lock/unlock must be structured to always unlock, even on error paths.
  • The easiest safe pattern is defer after a successful lock.

    Mutex and happens-before

    Mutexes are not only about mutual exclusion. They also publish memory safely.

    If goroutine A writes shared fields under the mutex and then calls Unlock, and goroutine B later successfully calls Lock on the same mutex, B is guaranteed to see A’s writes.

    This is exactly the kind of statement grounded in the Go memory model.

    Typical deadlock patterns

  • Self-deadlock: trying to Lock the same non-reentrant mutex twice in one goroutine.
  • Lock order inversion: goroutine 1 locks A then B, goroutine 2 locks B then A.
  • If you have multiple locks, define and follow a strict global lock order.

    !Deadlock via lock order inversion and the fixed lock ordering

    sync.RWMutex

    An RWMutex allows:

  • multiple concurrent readers (RLock/RUnlock)
  • a single writer (Lock/Unlock) that excludes both readers and other writers
  • When RWMutex helps (and when it doesn’t)

    Use it when:

  • the protected data is read frequently
  • writes are relatively rare
  • the read-side critical section is short
  • Avoid it when:

  • read-side work is long (read lock held too long)
  • write latency matters (writers may wait behind many readers)
  • your “reads” actually mutate state (stats, LRU updates)
  • In interviews, it’s good to say that RWMutex is a performance tool with tradeoffs, not a universal upgrade over Mutex.

    Two common pitfalls

  • Trying to upgrade: holding RLock and then calling Lock will deadlock.
  • Copying: like Mutex, RWMutex must not be copied after use.
  • sync.WaitGroup

    A WaitGroup lets one goroutine wait until a set of goroutines finishes.

    The mental model

  • Add(n) sets how many Done() calls you must observe before Wait() unblocks.
  • Done() is Add(-1).
  • !WaitGroup as a counter that gates Wait() until it reaches zero

    The core rule: call Add before starting goroutines

    A standard safe pattern:

    Calling Add concurrently with Wait is a classic bug and can panic.

    WaitGroup and memory visibility

    A WaitGroup is primarily for waiting for completion, but the practical guarantee you rely on is: once Wait() returns, the waiting goroutine can safely observe the effects produced by the completed goroutines as long as there is no data race.

    The interview-safe framing:

  • WaitGroup coordinates lifetimes.
  • Correct observation of shared memory still requires proper synchronization (mutexes/channels/atomics) if there are concurrent accesses.
  • sync.Once

    Once runs a function exactly once, even if multiple goroutines call it.

    Typical use: lazy initialization.

    Key properties

  • If multiple goroutines call Do, only one runs the function.
  • The others block until the function finishes.
  • After Do returns, the initialized data is safely published.
  • Pitfall: Do must not block forever

    If the function passed to Do deadlocks or blocks forever, every caller will block forever.

    Also avoid calling once.Do recursively (directly or indirectly) on the same Once.

    sync.Cond

    Cond is a condition variable: it lets goroutines wait until some condition becomes true, while releasing a mutex during the wait.

    You use it when channel-based designs are awkward, typically when:

  • you have multiple reasons a goroutine may proceed
  • you need to wake one waiter (Signal) or all waiters (Broadcast)
  • the shared state is best expressed as a predicate over fields protected by a mutex
  • A correct pattern always uses a predicate loop.

    Why the for loop matters

    You re-check the condition because:

  • wakeups can be spurious
  • another goroutine could consume the resource first
  • Broadcast wakes many goroutines, but only some may proceed
  • Cond vs channels

    A good interview line:

  • Prefer channels for clear ownership and message passing.
  • Use Cond when you already have shared state under a mutex and the coordination is predicate-based.
  • sync.Pool

    Pool is for reusing temporary objects to reduce allocations and GC pressure. It is not a general cache.

    The critical property

    Objects in a pool may be dropped at any time, especially across GC cycles. You must assume:

  • Get can return a new object (via New) even if you previously Put
  • items are not guaranteed to persist
  • This is why Pool is great for buffers and scratch objects, but wrong for “store and retrieve later” logic.

    Pool and correctness

  • Do not put objects back while they are still referenced elsewhere.
  • Reset objects before reuse to avoid data leaks.
  • This topic ties directly to the earlier GC discussion: reducing allocations reduces GC work, which can improve latency and throughput.

    Common interview comparisons and when to choose what

    Mutex vs channel

  • Use mutex when you have shared state and you need to enforce invariants.
  • Use channel when you want to transfer ownership of data or model a stream of events.
  • You can often convert between designs:

  • a “single goroutine owning a map” + requests over channels
  • or “many goroutines sharing a map” + mutex
  • Interviewers like to hear that you can reason about both and pick the simplest.

    WaitGroup vs channel close

    Both can indicate completion:

  • A WaitGroup is explicit and great for “wait for N goroutines”.
  • Closing a results channel is great for “range until done” pipelines.
  • In many real designs you combine them: use WaitGroup to know when to close the fan-in results channel.

    RWMutex vs Mutex

  • Start with Mutex.
  • Reach for RWMutex only if you have evidence (profiling) and a read-heavy workload.
  • The top pitfalls list (high signal on interviews)

  • Copying a struct containing a Mutex/RWMutex after use.
  • Forgetting to Unlock (especially on early returns); prefer defer.
  • Lock order inversion leading to deadlocks.
  • Calling WaitGroup.Add after goroutines started, or concurrently with Wait.
  • Assuming sync.Pool is a cache.
  • Using Cond.Wait without a predicate loop.
  • What you should be able to say out loud

  • Mutex gives both exclusion and safe publication: Unlock happens-before a later Lock.
  • WaitGroup is for lifetimes, not for protecting shared memory.
  • Once is the idiomatic safe lazy init and publishes initialized state.
  • Cond requires a predicate loop and is for shared-state coordination.
  • Pool is for temporary object reuse and items may disappear across GC.
  • This wraps the core of Go concurrency: goroutines, channels, and sync. In interviews, most “hard” questions are combinations of these tools plus the memory model (happens-before) and correct cancellation/termination.

    6. Race Conditions and Testing: race detector, Benchmarks, Profiling

    Race Conditions and Testing: race detector, Benchmarks, Profiling

    Эта статья завершает линию курса про конкурентность: после goroutines, channels и sync логично перейти к тому, как в реальности доказывать корректность и производительность.

    На собеседовании часто проверяют не только знание примитивов, но и инженерный подход:

  • как вы находите data race и race condition
  • как вы пишете тесты для конкурентного кода так, чтобы они были устойчивыми
  • как вы измеряете производительность через benchmarks
  • как вы находите узкие места через profiling
  • Инструменты, которые стоит знать и уметь воспроизвести голосом и командой в терминале:

  • race detector: Data Race Detector
  • тестирование и бенчмарки: package testing
  • профилирование: Profiling Go Programs
  • pprof: package net/http/pprof и package runtime/pprof
  • Race condition и data race

    Термины часто путают, а на интервью на это смотрят внимательно.

  • Data race: две goroutine конкурентно обращаются к одной и той же памяти, и хотя бы одно обращение это запись, при этом между ними нет синхронизации. Это нарушает требования модели памяти Go, и поведение становится недостоверным. Опора: The Go Memory Model.
  • Race condition: более широкое понятие. Логическая ошибка из-за недетерминированного порядка событий. Race condition может быть и без data race, например из-за неверной координации через каналы.
  • Пример race condition без data race: результат зависит от того, какой case в select сработает первым.

    Пример data race: общий map читается и пишется из разных goroutine без Mutex.

    !Схема различия между конкурентным доступом без синхронизации и доступом с happens-before

    Race detector

    Race detector в Go это инструмент, который в рантайме обнаруживает data race.

    Как запускать

    Самая практичная команда для проектов:

    Для точечного теста:

    Race detector можно включать и для бенчмарков:

    Heap профиль:

    Трейс:

    Дальше анализ:

    Или для трейсинга:

    Документация инструментов:

  • pprof: cmd/pprof
  • trace: cmd/trace
  • Профили в работающем сервисе через HTTP

    Для сервисов часто подключают net/http/pprof.

    И поднимают отдельный порт для диагностики. Справка: package net/http/pprof.

    На интервью важно подчеркнуть безопасность:

  • профили обычно включают только в internal-сетях
  • защищают auth или firewall
  • Как связывать profiling с конкурентностью

    Типичный цикл рассуждения, который ожидают:

  • Мы видим задержки.
  • Запускаем -race, чтобы исключить data race.
  • Смотрим CPU/heap профили.
  • Если проблема похожа на ожидание, смотрим block/mutex профили.
  • Если непонятно “когда и почему goroutine простаивают”, снимаем trace.
  • !Блок-схема выбора инструментов: race detector, benchmarks, pprof, trace

    Что полезно проговаривать на собеседовании

  • Data race это нарушение модели памяти, и порядок в коде не создаёт happens-before.
  • go test -race ./... это стандартная практика, но она не даёт математического доказательства отсутствия гонок.
  • Тесты конкурентности должны синхронизироваться событиями (каналы, WaitGroup, context), а не Sleep.
  • Бенчмарки измеряют код внутри цикла b.N, подготовку нужно выносить до ResetTimer.
  • Для производительности нужен связанный набор: -benchmem для аллокаций, pprof для CPU/heap, mutex/block профили для contention, trace для планировщика.
  • 7. Interview Practice: Typical Tasks, Code Review, and System Thinking

    Interview Practice: Typical Tasks, Code Review, and System Thinking

    Эта статья связывает все предыдущие темы курса в формат реального собеседования.

    Ранее мы освежили:

  • язык Go: типы, интерфейсы, ошибки
  • модель памяти: указатели, escape analysis, GC, happens-before
  • конкурентность: goroutines, каналы, select, отмена
  • sync: Mutex, RWMutex, WaitGroup, Once, Cond, Pool
  • практика: -race, тесты, бенчмарки, профили
  • Теперь цель другая: научиться решать типовые задачи и объяснять решения так, как этого ждут на интервью. Часто оценивают не только код, но и мышление: корректность, упорядочивание, завершение горутин, дизайн API, обработку ошибок, тестируемость.

    Как обычно устроено Go-собеседование

    Типовые форматы:

  • лайв-кодинг: написать функцию или небольшой компонент
  • разбор багов: найти race/deadlock/leak
  • code review: показать, что вы видите риски и улучшения
  • системный дизайн: спроектировать компонент сервиса с конкурентностью
  • Удобная стратегия ответа:

  • Уточнить контракт: входы/выходы, ошибки, отмена, порядок, ограничения.
  • Сказать про корректность: общая память, happens-before, отсутствие data race.
  • Сказать про завершение: где и как горутины остановятся.
  • Сказать про нагрузку: backpressure, лимиты параллелизма, аллокации.
  • Сказать про проверку: тесты, go test -race, профили.
  • Типовые задачи и что в них проверяют

    Ниже задачи сгруппированы так, чтобы вы отрабатывали одновременно язык, конкурентность и синхронизацию.

    Задачи на язык: типы, интерфейсы, ошибки

    Частые темы:

  • спроектировать небольшой интерфейс и реализовать его
  • корректно оборачивать ошибки и различать причины (errors.Is, errors.As)
  • не поймать ловушку nil-интерфейса
  • не создать лишние аллокации из-за интерфейсов и замыканий
  • Пример: функция-обёртка, которая добавляет контекст к ошибкам и сохраняет исходную причину.

    Что важно проговорить:

  • используем %w, чтобы дальше можно было сделать errors.Is или errors.As
  • текст обёртки должен добавлять контекст: что делали и с чем
  • Ссылки:

  • Package errors
  • Package fmt
  • Задачи на конкурентность: fan-out, fan-in, worker pool

    Самый частый класс задач: обработать много задач конкурентно, собрать результаты, не потерять ошибки, корректно завершиться по отмене.

    Минимальный скелет worker pool с отменой через context:

    Что тут проверяют:

  • есть ли backpressure: jobCh блокирует отправителя, если воркеры не успевают
  • не утекут ли горутины при отмене: все select слушают ctx.Done()
  • кто закрывает каналы и почему: jobCh закрывает производитель, resCh закрывает координатор после wg.Wait()
  • есть ли гонки: общие структуры не модифицируются без синхронизации
  • Если интервьюер допускает использование готовых примитивов, часто уместно упомянуть errgroup.

  • Package context
  • Go blog: Pipelines and cancellation
  • Package errgroup
  • !Схема владения каналами, fan-out/fan-in и распространения отмены

    Задачи на каналы и select: timeouts, cancellation, merge

    Типовые формулировки:

  • объединить несколько входных каналов в один выходной
  • сделать таймаут ожидания события
  • завершить работу по done или context
  • Классика: merge (fan-in) с корректным закрытием результата.

    Что важно проговорить:

  • out закрывает только координатор после завершения всех читателей
  • range ch корректно завершается по закрытию входа
  • если нужен ранний выход по отмене, добавляется select с ctx.Done() внутри каждой горутины
  • Отдельный популярный вопрос: почему чтение из nil-канала блокируется навсегда и как это используется, чтобы “выключать” case в select.

  • Go specification: Select statements
  • Задачи на sync: правильная защита состояния

    Здесь часто дают небольшой компонент: счётчик, кеш, очередь, агрегатор метрик.

    Счётчик с мьютексом:

    Что проверяют:

  • мьютекс не копируется после первого использования
  • defer ставится после успешного Lock
  • понимание: Unlock happens-before последующего Lock на том же мьютексе, значит данные корректно “публикуются”
  • Вопрос на понимание RWMutex: почему он может ухудшить ситуацию при длинных чтениях или при частых записях.

  • Package sync
  • The Go Memory Model
  • Задачи “найти баг”: гонка, deadlock, leak

    Обычно дают небольшой фрагмент и просят объяснить, что может пойти не так.

    Частые источники проблем:

  • замыкание на переменную цикла
  • WaitGroup.Add вызывается конкурентно с Wait
  • закрытие канала “не владельцем” (или закрытие при нескольких отправителях)
  • горутина навсегда ждёт чтения/записи без пути отмены
  • Мини-правила, которые хорошо звучат вслух:

  • канал закрывает тот, кто отправляет значения, и кто точно знает, что отправок больше не будет
  • каждая горутина должна иметь путь завершения
  • если есть общая память, нужен явный happens-before через каналы, мьютексы или атомики
  • Code review на интервью: что проверять системно

    На code review обычно смотрят не на вкус, а на риск: корректность, безопасность, производительность, поддерживаемость.

    Чек-лист корректности конкурентности

  • Есть ли общий map/slice/struct, который читают и пишут из разных горутин без синхронизации?
  • Есть ли понятный owner у каждого канала: кто создаёт, кто пишет, кто закрывает?
  • Есть ли путь отмены: context или done-канал, который реально используется в select?
  • Не может ли select с default превратиться в busy loop?
  • Не держим ли Lock на время I/O или долгих операций?
  • Чек-лист жизненного цикла и завершения

  • Горутины точно завершаются во всех ветках ошибок?
  • Каналы точно закрываются и закрываются один раз?
  • Нет ли сценария, где writer блокируется навсегда, потому что reader ушёл?
  • Чек-лист ошибок и API

  • Ошибки оборачиваются с контекстом (%w), а не теряются?
  • Разделяются ли “ожидаемые” ошибки и “программные” (где уместен panic)?
  • Не возвращается ли nil-интерфейс с динамическим типом?
  • Чек-лист производительности

  • Нет ли лишних аллокаций в горячем месте (например, из-за конвертации в interface или постоянных time.After в цикле)?
  • Не создаётся ли горутина на каждый элемент без лимита?
  • Не используется ли sync.Pool как “кеш”, хотя он не гарантирует сохранность объектов?
  • Полезные инструменты, которые уместно предложить прямо на интервью:

  • go test -race ./...
  • бенчмарк с -benchmem
  • mutex/block профили, если видите contention
  • Data Race Detector
  • Go blog: Profiling Go Programs
  • Package net/http/pprof
  • System thinking: как рассуждать о компоненте, а не о функции

    На системной части собеседования часто достаточно “компонента в миниатюре”: например, обработчик запросов, который параллельно ходит во внешние сервисы, кеширует результаты и ограничивает нагрузку.

    Вопросы, которые стоит задать до дизайна

  • Какие SLA: важнее latency или throughput?
  • Что делать при частичном успехе: вернуть частичный результат или упасть целиком?
  • Нужны ли дедлайны: общий context от запроса?
  • Какая семантика порядка: результаты должны быть в порядке входа или можно как готово?
  • Какой объём данных: можно ли буферизовать всё в памяти?
  • Конструктор решений: три оси

    Таблица, которая помогает объяснять выбор примитивов:

    | Ось | Вариант | Когда выбирать | Риски | |---|---|---|---| | Разделение данных | shared memory + Mutex | общий кеш, общие счётчики, инварианты | contention, deadlock, копирование мьютекса | | Передача владения | каналы и пайплайны | поток задач и событий, worker pool | неправильное закрытие каналов, утечки горутин | | Публикация результата | WaitGroup или закрытие канала | “дождаться всех” или “читать пока не закроют” | Add рядом с Wait, забыли закрыть |

    Правильная формулировка для интервью: вы выбираете инструмент по модели данных.

  • если данные общие и есть инварианты, берёте Mutex
  • если хотите очередь работ и контроль параллелизма, берёте каналы
  • Backpressure и лимиты параллелизма

    Очень частая ошибка дизайна: “на каждый запрос запускаем горутину на каждый элемент”. Корректнее почти всегда:

  • ограничить параллелизм worker pool
  • или семафором на буферизированном канале
  • Что важно проговорить:

  • семафор ограничивает параллелизм, но не защищает общие данные
  • это снижает риск перегрузки внешних зависимостей
  • Отмена и таймауты как часть контракта

    Если есть context, его стоит протягивать везде, где есть ожидание:

  • ожидание задач (select с ctx.Done())
  • отправка в канал (чтобы не зависнуть на забитом буфере)
  • ожидание результата
  • Ссылки:

  • Package context
  • Package time
  • Наблюдаемость и проверяемость

    Даже в учебном дизайне полезно назвать точки, где вы будете проверять и измерять:

  • тесты конкурентности без time.Sleep, с событиями и таймаутами
  • регулярный прогон -race
  • бенчмарки для hot path
  • pprof/trace, если “всё ждёт” или “всё медленно”
  • Это напрямую продолжает предыдущую статью курса про тестирование и профили.

    Как “продавать” решение на интервью

    Хороший ответ обычно включает:

  • короткий контракт
  • объяснение синхронизации: где happens-before
  • объяснение завершения: кто закрывает что и почему
  • объяснение ограничений: параллелизм, буферы, отмена
  • план проверки: тест, -race, бенчмарк при необходимости
  • Если вы застряли, полезная техника: проговорить один инвариант и один сценарий завершения.

    > “У каждого канала есть владелец закрытия, а каждая горутина слушает ctx.Done() и имеет путь завершения. Общая память либо не используется, либо защищена мьютексом.”

    Эта статья закрывает курс практикой собеседований: вы теперь не просто знаете примитивы Go, а умеете применять их как систему, объяснять корректность через модель памяти и находить риски в чужом коде.