← Back
justxor

justxor/sobesrazborgoogle

Разбор актуальных вопросов с собеседований для программистов

View on GitHub ↗
Stars
109
Forks
15
Watchers
109
Open issues
0
Contributors
1
Language
—
License
—
Default branch
main
Created Sep 25, 2026Updated Sep 27, 2026

Star growth

Today—
This week—
This month—

Star history will appear here once this repo has been tracked for a couple of days.

README

Go собеседование 2026: вопросы, ответы и задачи (Golang Interview Questions)

Go License: MIT PRs Welcome

Полная подготовка к собеседованию на Go-разработчика (Golang backend developer) в 2026 году: 225+ вопросов с разборами, 40 задач «что выведет код», 27 задач с live coding с решениями на Go и тестами, конкурентность, runtime, GC, продвинутые темы для Senior (внутренности каналов, select и Swiss Tables, компилятор и PGO, unsafe, lock-free, распределённые системы), system design и всё новое в Go 1.22–1.27.

Актуально на сентябрь 2026 (Go 1.27): generic-методы, encoding/json/v2, Green Tea GC, new(expr), errors.AsType, testing/synctest, sync.WaitGroup.Go, container-aware GOMAXPROCS, Swiss Tables в map.

⭐ Поставьте звезду, чтобы не потерять, — README регулярно пополняется вопросами с реальных собеседований.

Полезные популярные ресурсы для разработчиков ❓ Go Академия - реальные тесты для проверки своих знаний.

💡 Go собеседование - огромное количество разобранных вопросов с собеседований Go разработчика.

⚡️Machine learning- показываем на примере как использовать AI, который может генерировать готовые базы данных, код, разбираем все что нужно знать в области ИИ.

📚 Golang книги - самая большая библиотека бесплатных GO книг   💼 Вакансии Go — вакансии и фриланс проекты для Go разработчиков.

📂 Папка самых полезных каналовдля Go разработчиков.


Содержание

Теория

  • Основы Go: вопросы на собеседовании с ответами
  • Слайсы, map и строки в Go: устройство и вопросы на собеседовании
  • Интерфейсы, структуры и методы в Go
  • Обработка ошибок, panic и recover в Go
  • Горутины и каналы: вопросы на собеседовании по конкурентности в Go
  • sync, atomic и модель памяти Go
  • Runtime Go: планировщик, стек, escape analysis и сборщик мусора
  • Дженерики в Go (1.18 → 1.27): вопросы и ответы
  • context.Context в Go: отмена, таймауты, значения
  • Тестирование, бенчмарки и профилирование Go
  • Go Backend на собеседовании: HTTP, gRPC, базы данных, брокеры сообщений
  • Архитектура Go-сервисов и паттерны проектирования
  • 🔥 Продвинутый Go: внутренности рантайма, компилятор, unsafe и производительность (Senior+)
  • Что нового в Go 1.22–1.27: шпаргалка к собеседованию 2026
  • System Design на собеседовании Go-разработчика (Middle+/Senior)
  • 🔥 Распределённые системы и надёжность: вопросы Senior Go-разработчику
  • Как проходит собеседование Go-разработчика в 2026 году

Практика

  • «Что выведет этот код?» — 25 каверзных задач по Go с собеседований
  • 🔥 «Что выведет этот код?» — Senior-уровень: ещё 15 задач
  • Задачи с Go-собеседований: решения, разборы и тесты
    • Two Sum — найти два числа с заданной суммой
    • Правильная скобочная последовательность
    • Пересечение двух слайсов
    • RLE-сжатие строки
    • Бинарный поиск: lower bound и сдвинутый массив
    • Связный список: разворот, цикл, середина, слияние
    • Group Anagrams — группировка анаграмм
    • Longest Substring Without Repeating Characters
    • Merge Intervals — слияние отрезков
    • Top K Frequent — K самых частых элементов
    • LRU Cache на Go (generics + container/list)
    • Worker Pool на Go
    • Fan-in: слить N каналов в один
    • Pipeline (конвейер) на каналах
    • Первый успешный ответ из N реплик (с таймаутом)
    • Кэш с TTL (in-memory)
    • Graceful shutdown HTTP-сервера
    • Параллельные запросы с лимитом и отменой (свой errgroup)
    • Rate Limiter (token bucket)
    • Батчер: пачка по размеру или по времени
    • Pub/Sub брокер в памяти
    • Singleflight — защита от «эффекта стада» (cache stampede)
    • 🔥 Circuit Breaker на Go
    • 🔥 Consistent hashing с виртуальными узлами
    • 🔥 Шардированная конкурентная map (generics)
    • 🔥 Lock-free стек Трайбера на atomic.Pointer
    • 🔥 Retry с экспоненциальной задержкой и jitter
  • Чек-лист подготовки к собеседованию Go-разработчика
  • Топ-20 вопросов на собеседовании Go
  • Топ-10 вопросов Senior-уровня
  • Полезные ресурсы

Топ-20 вопросов на собеседовании Go

Если времени мало — начните с этих:

  1. Как устроен слайс и что делает append?
  2. Как устроена map? Почему она не потокобезопасна?
  3. Почему err != nil, если вернули nil-указатель?
  4. Value receiver vs pointer receiver
  5. Как работает defer?
  6. Горутина vs поток ОС
  7. Поведение nil и закрытых каналов
  8. Кто закрывает канал?
  9. Что такое goroutine leak и как найти?
  10. Модель G-M-P
  11. Как работает GC? Что такое Green Tea?
  12. Escape analysis
  13. Mutex vs RWMutex vs atomic
  14. Модель памяти и happens-before
  15. Зачем context и почему нужен cancel()?
  16. errors.Is vs errors.As
  17. Поймает ли recover панику из горутины?
  18. Как реализованы дженерики?
  19. Напишите worker pool
  20. Что нового в последних версиях Go?

Топ-10 вопросов Senior-уровня

Если идёте на Senior/Lead:

  1. Как устроен канал внутри и что такое прямая передача между стеками?
  2. Как работает select и почему блокировки берутся по адресу канала?
  3. Hybrid write barrier: почему паузы GC субмиллисекундные
  4. Swiss Tables в map: H1/H2, группы, расширяемое хэширование
  5. Инлайнинг, BCE и PGO
  6. Правила unsafe.Pointer и zero-copy []byte ↔ string
  7. Утечки памяти при наличии GC
  8. Рост p99 при «нормальном» CPU-профиле
  9. Ретраи, circuit breaker и идемпотентность
  10. Распределённый лок и fencing token

Теория: вопросы и ответы

Основы Go: вопросы на собеседовании с ответами

Раздел для Junior/Middle. Эти вопросы задают в первые 10 минут, чтобы понять, писали ли вы на Go по-настоящему.


1. Чем Go отличается от других языков? Почему его выбирают для backend?

  • Компилируется в один статический бинарник, быстрый старт → удобно в Docker/Kubernetes.
  • Встроенная конкурентность: горутины (стартовый стек ~2 КБ) + каналы, планировщик M:N в рантайме.
  • Сборщик мусора с низкими паузами (concurrent mark-sweep; с Go 1.26 по умолчанию — Green Tea GC).
  • Минималистичный синтаксис, один стиль (gofmt), быстрая компиляция.
  • Сильная стандартная библиотека: net/http, context, encoding/json, testing, log/slog.
  • Нет наследования — композиция через встраивание (embedding) и неявные интерфейсы.

2. Что такое zero value? Назовите нулевые значения типов

Любая переменная без явной инициализации получает нулевое значение:

Тип Zero value
int, float64 0
string ""
bool false
указатель, слайс, map, канал, функция, интерфейс nil
структура все поля — их zero value
массив все элементы — zero value

Идиома «useful zero value»: sync.Mutex, bytes.Buffer, strings.Builder готовы к работе без конструктора. Но запись в nil-map → panic.

3. var x int, x := 0, new(int) — в чём разница?

  • var x int — объявление с zero value; работает и на уровне пакета.
  • x := 0 — короткое объявление, только внутри функций; слева должна быть хотя бы одна новая переменная.
  • new(T) — выделяет память под T, возвращает *T на zero value.
  • Go 1.26+: new принимает выражение: p := new(42) или new(time.Now()) — удобно для опциональных полей-указателей в JSON/protobuf.

4. new vs make

  • make — только для слайсов, map и каналов: инициализирует внутреннюю структуру и возвращает значение (не указатель).
  • new — для любого типа, возвращает указатель на zero value. new([]int) вернёт *[]int, указывающий на nil-слайс.

5. Что такое shadowing (затенение) переменных и чем опасно?

var err error
if true {
    x, err := f() // новая err во внутреннем скоупе!
    _ = x
}
return err // всегда nil

Лечится = вместо := или линтером (go vet -vettool=shadow, govet в golangci-lint).

6. Как работает defer? В каком порядке выполняются? Когда вычисляются аргументы?

  • Отложенные вызовы выполняются при выходе из функции (не блока) в порядке LIFO.
  • Аргументы вычисляются в момент defer, а не при выполнении.
  • defer может изменить именованные возвращаемые значения.
func f() (n int) {
    defer func() { n *= 2 }()
    return 3 // n = 3, потом defer → 6
}

for i := 0; i < 3; i++ { defer fmt.Print(i) } // 210

Ловушка: defer в цикле (например, defer f.Close() для 10 000 файлов) — всё закроется только в конце функции. Выносите тело цикла в отдельную функцию.

7. Что изменилось в циклах for в Go 1.22?

Переменная цикла теперь создаётся заново на каждой итерации (при go 1.22+ в go.mod).

for i := 0; i < 3; i++ {
    go func() { fmt.Println(i) }() // до 1.22: чаще всего 3 3 3; с 1.22: 0 1 2 в любом порядке
}

Также появился for i := range 10 (range по целому) и в 1.23 — range-over-func (итераторы, iter.Seq).

8. Что такое iota?

Счётчик внутри const (...), начинается с 0 и растёт на каждой строке.

type Weekday int
const (
    Sunday Weekday = iota // 0
    Monday                // 1
    _                     // 2 — пропуск
    Wednesday             // 3
)
const (
    _  = iota
    KB = 1 << (10 * iota) // 1024
    MB                    // 1048576
)

9. Типизированные и нетипизированные константы

const x = 10 — нетипизированная, имеет «идеальную» точность и приводится к нужному типу при использовании: var f float64 = x работает. const y int = 10 — типизированная, var f float64 = y не скомпилируется.

10. Передача аргументов: по значению или по ссылке?

Всегда по значению (копируется). Но копия слайса/map/канала/указателя/интерфейса содержит указатель на те же данные, поэтому изменения элементов видны снаружи. Для слайса: s[0] = 1 видно вызывающему, а append — нет (если произошла реаллокация или длина вызывающего не изменилась).

11. Что такое замыкание (closure)?

Функция, захватывающая переменные из внешней области по ссылке:

func counter() func() int {
    n := 0
    return func() int { n++; return n }
}

n «убегает» в кучу (escape analysis).

12. Какие есть модификаторы доступа?

Только два уровня видимости: идентификатор с заглавной буквы экспортируется из пакета, со строчной — нет. Плюс каталог internal/ — импортируется только из родительского дерева.

13. Что такое init()? Порядок инициализации пакета

  1. Инициализируются импортированные пакеты (рекурсивно, каждый один раз).
  2. Переменные уровня пакета — в порядке зависимостей.
  3. Все init() в порядке файлов (как их передал go build, обычно по алфавиту) и порядке объявления.
  4. main(). В одном файле может быть несколько init. Злоупотреблять не стоит: неявные побочные эффекты мешают тестам.

14. Что такое goto, метки, break с меткой?

outer:
for _, row := range grid {
    for _, v := range row {
        if v == target { break outer }
    }
}

Частый вопрос: break внутри select/switch в цикле выходит только из select/switch, а не из цикла.

15. Чем отличаются type A B и type A = B?

  • type A B — новый тип с тем же underlying-типом; методы B не наследуются; нужна явная конвертация.
  • type A = B — алиас, это тот же тип. С Go 1.24 алиасы могут быть параметризованными: type Set[T comparable] = map[T]struct{}.

16. Как устроен switch в Go?

  • break не нужен, fallthrough — явно.
  • switch без условия = цепочка if/else.
  • Type switch: switch v := x.(type) { case int: ... }.

17. Что такое rune и byte?

byte = uint8, rune = int32 (кодовая точка Unicode). Подробно — в следующем разделе.

18. Как в Go сделать enum?

Отдельный тип + iota + метод String() (генерируется stringer: //go:generate stringer -type=Weekday). Для валидации — метод IsValid().

19. Что делает _ (blank identifier)?

Игнорирует значение, импорт только ради init (import _ "github.com/lib/pq"), проверка реализации интерфейса на этапе компиляции:

var _ io.Reader = (*MyReader)(nil)

20. Что такое go.mod, go.sum, toolchain, workspace?

  • go.mod — путь модуля, минимальная версия Go (go 1.25), зависимости (MVS — minimal version selection).
  • go.sum — криптографические хэши зависимостей.
  • toolchain go1.27.0 — рекомендуемый тулчейн; Go может скачать его сам (GOTOOLCHAIN).
  • go.work — несколько модулей локально без replace.
  • С Go 1.24 — директива tool в go.mod для зависимостей-утилит (go get -tool golang.org/x/tools/cmd/stringer, запуск через go tool stringer).

Слайсы, map и строки в Go: устройство и вопросы на собеседовании

Самый «горячий» раздел. Задачи «что выведет код» со слайсами есть почти на каждом Go-собесе.


Слайсы

1. Как устроен слайс?

Структура из трёх слов (24 байта на amd64):

type slice struct {
    array unsafe.Pointer // указатель на базовый массив
    len   int
    cap   int
}

Слайс — «окно» в массив. Несколько слайсов могут делить один массив.

2. Массив vs слайс

Массив — значение фиксированного размера, размер — часть типа ([3]int ≠ [4]int), копируется целиком при присваивании и передаче. Массивы comparable (можно == и ключом map), слайсы — нет.

3. Как работает append и рост capacity?

  • Если len < cap — пишем в тот же массив, возвращаем слайс с len+1.
  • Иначе — выделяем новый массив, копируем. Рост (Go 1.18+): до 256 элементов — ×2, дальше плавно к ×1.25 (newcap += (newcap + 3*256) / 4), затем округление до size class аллокатора.
  • Поэтому всегда s = append(s, x).

4. Классическая ловушка: общий базовый массив

a := []int{1, 2, 3, 4}
b := a[:2]          // len 2, cap 4
b = append(b, 99)   // пишет в a[2]!
fmt.Println(a)      // [1 2 99 4]

Защита — full slice expression a[:2:2] (cap = 2) → append вынужден реаллоцировать.

5. Что выведет?

func add(s []int) { s = append(s, 4); s[0] = 100 }

s := make([]int, 3, 10)
add(s)
fmt.Println(s, len(s)) // [100 0 0] 3

s[0] = 100 виден (общий массив), а новый элемент — нет: len у вызывающего остался 3. Хотя s[:4] покажет 4.

6. nil-слайс vs пустой слайс

var a []int      // nil, len 0
b := []int{}     // не nil, len 0

Оба безопасны для len, range, append. Разница: a == nil → true; json.Marshal даёт null vs []. В API обычно возвращают nil, в JSON-ответах — пустой, если фронт ждёт массив.

7. Как скопировать слайс?

copy(dst, src) копирует min(len(dst), len(src)) элементов. Или slices.Clone(s), или append([]T(nil), s...).

8. Как удалить элемент из слайса?

s = slices.Delete(s, i, i+1)        // Go 1.21+, с 1.22 обнуляет хвост (нет утечек указателей)
s = append(s[:i], s[i+1:]...)        // вручную, O(n)
s[i] = s[len(s)-1]; s = s[:len(s)-1] // O(1), если порядок не важен

9. Утечка памяти через подслайс

func head(b []byte) []byte { return b[:10] } // держит весь мегабайтный массив

Решение — скопировать: slices.Clone(b[:10]).

10. Пакет slices (Go 1.21+) — что знать

Sort, SortFunc, BinarySearch, Contains, Index, Max, Min, Reverse, Compact, Equal, Insert, Delete, Clone, Grow, Chunk (1.23, итератор), Collect, Sorted, Values (итераторы, 1.23).


Map

11. Как устроена map в Go?

  • До Go 1.24: хэш-таблица из бакетов по 8 элементов + overflow-бакеты, инкрементальная эвакуация при росте (load factor 6.5).
  • С Go 1.24: реализация на Swiss Tables — группы по 8 слотов с контрольным словом (7 бит хэша на слот), поиск через SIMD-подобное сравнение метаданных, таблицы до 1024 слотов, расширяемое хэширование (directory). Быстрее на 20–60% на поиске/вставке, меньше памяти.
  • Map — указатель на runtime.hmap/maps.Map, поэтому при передаче в функцию изменения видны.

12. Почему порядок итерации по map случайный?

Намеренно рандомизирован рантаймом, чтобы никто не полагался на порядок. Для стабильного порядка: slices.Sorted(maps.Keys(m)) (Go 1.23+).

13. Можно ли взять адрес элемента map? &m[k]

Нет — ошибка компиляции. При росте элементы перемещаются. По той же причине m[k].field = 1 для map[K]Struct не компилируется — нужно v := m[k]; v.field = 1; m[k] = v или хранить указатели.

14. Что будет при записи в nil-map? Чтении?

Чтение — zero value, len = 0, delete — no-op. Запись → panic: assignment to entry in nil map.

15. Потокобезопасна ли map?

Нет. Конкурентная запись (или запись + чтение) → рантайм может упасть с fatal error: concurrent map writes (это не panic, recover не поможет). Решения: sync.Mutex/RWMutex, sync.Map, шардирование.

16. Что может быть ключом map?

Любой comparable тип: числа, строки, bool, указатели, каналы, интерфейсы, массивы и структуры из comparable-полей. Нельзя: слайсы, map, функции. Интерфейс с несравнимым динамическим значением скомпилируется, но упадёт в рантайме.

17. Как проверить наличие ключа?

v, ok := m[k]. Для множества — map[T]struct{}.

18. Освобождает ли delete память?

Нет, число бакетов/групп не уменьшается. clear(m) (Go 1.21) очищает, но тоже не сжимает. Для сжатия — создать новую map.

19. sync.Map — когда использовать?

Оптимизирован для двух сценариев: ключ записывается один раз, а читается много (кэши), и горутины работают с непересекающимися наборами ключей. С Go 1.24 внутри — конкурентное HashTrieMap, стал быстрее. В остальных случаях map + RWMutex проще и типобезопаснее.


Строки

20. Как устроена строка?

Неизменяемая структура {ptr *byte, len int} (16 байт). Байты — обычно UTF-8. Подстрока s[i:j] не копирует данные.

21. Что вернёт len("привет")?

12 — байты. Число символов: utf8.RuneCountInString(s) → 6.

22. Чем отличается итерация по индексу и через range?

s := "héllo"
for i := 0; i < len(s); i++ { s[i] }   // байты (uint8)
for i, r := range s { }                  // руны; i — байтовый индекс начала руны: 0,1,3,4,5

Невалидный UTF-8 при range даёт utf8.RuneError (U+FFFD).

23. Почему нельзя s[0] = 'a'?

Строки неизменяемы. Нужно b := []byte(s); b[0] = 'a'; s = string(b) (две аллокации) или strings.Builder.

24. Как эффективно конкатенировать строки?

  • + в цикле — O(n²) из-за копирования.
  • strings.Builder с Grow(n) — одна аллокация.
  • strings.Join для слайса.
  • fmt.Sprintf — удобно, но медленнее (рефлексия, интерфейсы).

25. Конвертация string ↔ []byte — всегда ли копирование?

Семантически — да. Компилятор оптимизирует частные случаи: m[string(b)] при поиске в map, сравнение string(b) == "x", for range []byte(s). Без копирования вручную — unsafe.String(&b[0], len(b)) / unsafe.Slice(unsafe.StringData(s), len(s)), но только если данные больше не меняются.

26. Новые функции в strings/bytes

Cut (1.18), CutPrefix/CutSuffix (1.20), Lines, SplitSeq, FieldsSeq — итераторы (1.24), CutLast (1.27), bytes.Buffer.Peek (1.26).


Интерфейсы, структуры и методы в Go


1. Как устроен интерфейс внутри?

Два слова:

  • iface (непустой интерфейс): {tab *itab, data unsafe.Pointer}, где itab хранит тип и таблицу методов.
  • eface (any / interface{}): {_type *_type, data unsafe.Pointer}.

Интерфейс — это пара (динамический тип, значение).

2. Главная ловушка: nil-интерфейс ≠ интерфейс с nil-значением

type MyErr struct{}
func (*MyErr) Error() string { return "boom" }

func do() error {
    var e *MyErr = nil
    return e // интерфейс {type: *MyErr, value: nil}
}

fmt.Println(do() == nil) // false!

Интерфейс равен nil, только если и тип, и значение nil. Правило: возвращайте nil явно, а не типизированный nil-указатель.

3. Неявная реализация интерфейсов — плюсы и минусы

Тип реализует интерфейс автоматически, если имеет все методы (duck typing на этапе компиляции). Плюсы: нет зависимости от пакета с интерфейсом, легко мокать. Идиома: «Accept interfaces, return structs», интерфейсы объявляются на стороне потребителя и маленькие (io.Reader — 1 метод).

Проверка на этапе компиляции: var _ Storage = (*PgStorage)(nil).

4. Value receiver vs pointer receiver

Value func (t T) Pointer func (t *T)
Меняет исходный объект нет (копия) да
Копирование всей структуры 8 байт
Method set T ✔ ✘
Method set *T ✔ ✔

Следствие: если метод объявлен на *T, то значение T не реализует интерфейс:

type S struct{}
func (*S) M() {}
var _ I = S{}  // ошибка компиляции
var _ I = &S{} // ок

Правило: если хоть один метод на указателе (или есть мьютекс внутри) — делайте все на указателе.

5. Type assertion и type switch

v, ok := x.(string)   // безопасно
v := x.(string)       // panic, если не string
switch v := x.(type) {
case int, int64:     // v — тип x (any)
case fmt.Stringer:   // v — fmt.Stringer
}

6. Встраивание (embedding) — это наследование?

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

type Base struct{}
func (Base) Name() string   { return "base" }
func (b Base) Hello() string { return "hi " + b.Name() }

type Child struct{ Base }
func (Child) Name() string { return "child" }

Child{}.Hello() // "hi base" — не "hi child"!

Встраивание интерфейса в структуру — приём для частичных моков и декораторов.

7. Можно ли сравнивать структуры?

== работает, если все поля comparable. Структура со слайсом/map — ошибка компиляции. Интерфейсы с несравнимым значением внутри — panic в рантайме. Для глубокого сравнения в тестах — reflect.DeepEqual или github.com/google/go-cmp.

8. Пустая структура struct{} — зачем?

Занимает 0 байт (все такие значения могут иметь один адрес runtime.zerobase). Применения: множества map[K]struct{}, сигнальные каналы chan struct{}, типы-маркеры с методами.

9. Выравнивание полей (alignment/padding)

type Bad struct {  // 24 байта
    a bool   // 1 + 7 padding
    b int64  // 8
    c bool   // 1 + 7 padding
}
type Good struct { // 16 байт
    b int64
    a, c bool
}

Проверка: unsafe.Sizeof, линтер fieldalignment. Важно для горячих структур и 64-битных атомиков на 32-битных платформах (используйте atomic.Int64 — он выровнен).

10. Теги структур

Метаданные для рефлексии: json:"name,omitempty", db:"id", validate:"required". Читаются через reflect.StructTag.Get. В Go 1.24 появилась опция omitzero в encoding/json — пропускает zero value (в т.ч. time.Time{}) и учитывает метод IsZero(). В encoding/json/v2 (Go 1.27) — ещё строже и быстрее.

11. Можно ли определить метод для типа из другого пакета?

Нет. Только для типов своего пакета. Решение — type MyTime time.Time или обёртка-структура.

12. Что такое any? Когда его использовать?

Алиас interface{} (Go 1.18). Использовать минимально: теряется типобезопасность, значения упаковываются (boxing → часто аллокация). Сейчас большинство кейсов закрывают дженерики.

13. Функциональные опции (functional options)

type Option func(*Server)
func WithTimeout(d time.Duration) Option { return func(s *Server) { s.timeout = d } }
func NewServer(addr string, opts ...Option) *Server {
    s := &Server{addr: addr, timeout: 30 * time.Second}
    for _, o := range opts { o(s) }
    return s
}

Спрашивают как пример идиоматичного API в Go.

14. Когда интерфейс вызывает аллокацию?

При присваивании в интерфейс значение, не помещающееся в указатель (или чей адрес «убегает»), копируется в кучу. Маленькие целые (0–255) и нулевые значения рантайм берёт из статических таблиц. Проверяйте: go build -gcflags=-m.


Обработка ошибок, panic и recover в Go


1. Что такое error?

Встроенный интерфейс type error interface { Error() string }. Ошибки — обычные значения, возвращаются последним результатом.

2. Sentinel errors, кастомные типы, обёртки — когда что?

Способ Пример Проверка
Sentinel var ErrNotFound = errors.New("not found") errors.Is(err, ErrNotFound)
Свой тип type ValidationError struct{ Field string } errors.As(err, &ve) / errors.AsType[*ValidationError](err) (Go 1.26)
Обёртка fmt.Errorf("get user %d: %w", id, err) цепочка Unwrap
Непрозрачная fmt.Errorf("...: %v", err) разрывает цепочку — осознанно скрываем детали

3. errors.Is vs errors.As vs ==

  • == сравнивает только верхний уровень — после обёртки %w сломается.
  • errors.Is идёт по цепочке Unwrap() и сравнивает (или вызывает метод Is).
  • errors.As ищет в цепочке ошибку нужного типа и присваивает её.
  • Go 1.26: errors.AsType[T] — дженерик-версия без указателя на переменную:
if ve, ok := errors.AsType[*ValidationError](err); ok { log.Println(ve.Field) }

4. Несколько ошибок сразу

errors.Join(err1, err2) (Go 1.20) и fmt.Errorf("%w; %w", a, b) — ошибка с Unwrap() []error. errors.Is проверяет все ветки.

5. Как правильно оборачивать?

Добавлять контекст что делали, без слов «failed to»/«error»: fmt.Errorf("open config %q: %w", path, err). Итоговое сообщение читается как цепочка: load app: open config "a.yaml": no such file. Не логировать и возвращать одновременно — ошибка будет залогирована N раз.

6. Что такое panic? Когда её использовать?

Аварийное завершение: раскручивается стек, выполняются defer, затем процесс падает с трейсом. Использовать для ошибок программиста и невозможных состояний (нарушение инвариантов, MustCompile при инициализации). Для ожидаемых ошибок (сеть, ввод) — error.

7. Как работает recover?

Возвращает значение паники, только если вызван непосредственно в отложенной функции в той же горутине:

func safe(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v\n%s", r, debug.Stack())
        }
    }()
    fn()
    return nil
}

8. Поймает ли recover панику из другой горутины?

Нет. Паника в любой горутине без recover роняет весь процесс. Поэтому в каждой долгоживущей горутине (воркеры, обработчики) нужен свой defer recover. net/http восстанавливает панику в хендлере сам (и логирует), но горутины, запущенные из хендлера, — нет.

9. Что нельзя поймать recover?

fatal error рантайма: конкурентная запись в map, out of memory, stack overflow (goroutine stack exceeds 1000000000-byte limit), deadlock all goroutines are asleep.

10. panic(nil) — что будет?

С Go 1.21 panic(nil) превращается в *runtime.PanicNilError, и recover() возвращает не-nil. Раньше recover возвращал nil, и паника была «невидимой».

11. Порядок выполнения при панике

func main() {
    defer fmt.Println("1")
    defer func() { recover(); fmt.Println("2") }()
    defer fmt.Println("3")
    panic("boom")
}
// 3, 2, 1 — программа завершается нормально

12. Ошибки в defer f.Close() — теряем?

Для файлов на запись — да, и это баг: Close может вернуть ошибку сброса буфера. Правильно:

defer func() { err = errors.Join(err, f.Close()) }()

Горутины и каналы: вопросы на собеседовании по конкурентности в Go

Раздел, на котором «валится» больше всего кандидатов уровня Middle. Практика — в разделе задач.


1. Что такое горутина? Чем отличается от потока ОС?

Горутина Поток ОС
Стек стартует с 2 КБ, растёт копированием (до 1 ГБ на 64-бит) 1–8 МБ фиксированно
Создание ~сотни нс, в user space системный вызов, мкс
Переключение рантайм Go, сохраняются несколько регистров ядро, полный контекст
Планирование M:N планировщик Go (G-M-P) планировщик ОС
Идентификатор нет публичного ID (намеренно) TID

2. Конкурентность vs параллелизм

Конкурентность — структура программы (много независимых задач). Параллелизм — одновременное выполнение на нескольких ядрах. Go даёт конкурентность, а параллелизм зависит от GOMAXPROCS.

3. Как устроен канал?

runtime.hchan: кольцевой буфер (для буферизированных), sendx/recvx, очереди ожидающих отправителей и получателей (sendq/recvq из sudog), мьютекс. При небуферизированной передаче данные копируются прямо в стек ждущей горутины.

4. Буферизированный vs небуферизированный канал

  • Небуферизированный: отправка блокируется, пока получатель не заберёт — точка синхронизации (happens-before).
  • Буферизированный (make(chan T, n)): отправка блокируется только при полном буфере. Используется как очередь или семафор.

5. Таблица поведения каналов (выучить наизусть)

Операция nil-канал закрытый канал открытый канал
Чтение <-ch блок навсегда zero value, ok=false сразу ждёт данные
Запись ch <- блок навсегда panic ждёт место/получателя
close(ch) panic panic ок
len/cap 0 остаток в буфере как есть

6. Кто должен закрывать канал?

Отправитель, и только когда больше никто не будет писать. Получатель канал не закрывает. При нескольких отправителях — отдельная горутина wg.Wait(); close(ch) или сигнальный канал done. Закрывать канал не обязательно — GC соберёт; закрывают, чтобы сообщить получателям «данных больше нет».

7. Как работает select?

  • Ждёт, пока хотя бы один case готов; если готово несколько — выбирается случайно (равномерно), чтобы не было голодания.
  • default делает select неблокирующим.
  • select {} — блок навсегда.
  • case с nil-каналом никогда не срабатывает — приём для «отключения» веток.

8. Что такое goroutine leak? Как найти?

Горутина, заблокированная навсегда (ждёт канал, который никто не закроет/не прочитает). Растёт память, не освобождаются ресурсы. Поиск:

  • runtime.NumGoroutine() в метриках, /debug/pprof/goroutine?debug=2.
  • Go 1.26+ профиль goroutineleak (в Go 1.27 включён по умолчанию): GC находит горутины, заблокированные на примитивах, недостижимых из живых горутин — /debug/pprof/goroutineleak.
  • В тестах — go.uber.org/goleak, или testing/synctest (Go 1.25) — synctest.Test падает, если в «пузыре» остались заблокированные горутины.

9. Как ограничить число одновременных горутин?

Семафор на канале, worker pool, errgroup.SetLimit, golang.org/x/sync/semaphore (взвешенный). См. workerpool и parallel.

10. Как дождаться завершения горутин?

sync.WaitGroup. С Go 1.25 — wg.Go(func(){...}), который сам делает Add(1) и Done():

var wg sync.WaitGroup
for _, u := range urls {
    wg.Go(func() { fetch(u) })
}
wg.Wait()

Для ошибок и отмены — errgroup.Group.

11. Что выведет?

ch := make(chan int, 3)
ch <- 1; ch <- 2; ch <- 3
close(ch)
for v := range ch { fmt.Print(v) } // 123 — из закрытого канала дочитываются остатки
v, ok := <-ch                       // 0 false

12. Deadlock: когда рантайм его ловит?

fatal error: all goroutines are asleep - deadlock! — только если все горутины заблокированы. Если хоть одна жива (например, HTTP-сервер или тикер), частичный дедлок рантайм не заметит.

func main() {
    ch := make(chan int)
    ch <- 1 // deadlock: некому читать
}

13. Паттерны: generator, fan-in, fan-out, pipeline, worker pool, semaphore, pub/sub, or-done, tee

Всё с решениями и тестами — в разделе задач.

14. Как остановить горутину извне?

Никак принудительно. Только кооперативно: горутина сама проверяет ctx.Done() / закрытый done-канал.

15. Что такое runtime.Gosched(), runtime.Goexit(), LockOSThread?

  • Gosched — уступить процессор (сейчас почти не нужен: с Go 1.14 есть асинхронное вытеснение).
  • Goexit — завершить текущую горутину, выполнив defer (так работает t.FailNow).
  • LockOSThread — привязать горутину к потоку (cgo, OpenGL, namespace в Linux).

16. Каналы или мьютексы?

«Share memory by communicating» — каналы для передачи владения данными и координации (конвейеры, события). Мьютексы — для защиты состояния (кэш, счётчик). Каналы медленнее мьютекса примерно в разы (внутри тот же lock + планирование).


sync, atomic и модель памяти Go


1. Что такое data race? Как найти?

Одновременный доступ к одной переменной из ≥2 горутин, где хотя бы один — запись, без синхронизации. Поведение не определено (рваные значения, в т.ч. у интерфейсов и слайсов — многословных структур). Поиск: go test -race, go run -race (ThreadSanitizer, замедление ×2–20, память ×5–10). Находит только гонки, которые случились во время прогона.

2. Race condition vs data race

Data race — низкоуровневый конфликт доступа к памяти. Race condition — логическая ошибка из-за порядка событий (check-then-act), возможна и без data race: if m.Get(k) == nil { m.Set(k, v) } — каждый вызов под мьютексом, а вместе — нет.

3. sync.Mutex — как устроен?

Два режима:

  • Нормальный: новые горутины конкурируют с разбуженной; сначала спин (на многоядерных), потом парковка через семафор.
  • Голодания (starvation): если горутина ждёт > 1 мс, мьютекс передаётся строго по очереди FIFO. Мьютекс не реентерабельный — повторный Lock в той же горутине = дедлок. Нельзя копировать после использования (go vet → copylocks).

4. RWMutex — когда выгоден?

Много читателей, мало писателей, и критическая секция чтения не микроскопическая. Иначе overhead RWMutex больше, чем у Mutex. Писатель, ожидающий Lock, блокирует новых читателей (защита от голодания писателей) → рекурсивный RLock может дать дедлок.

5. sync.Once, OnceFunc, OnceValue, OnceValues

var getCfg = sync.OnceValue(func() *Config { return load() }) // Go 1.21
cfg := getCfg()

Если функция внутри Once.Do паникует, Once считается выполненным.

6. sync.Cond — зачем, если есть каналы?

Для broadcast-пробуждения многих ожидающих по изменению состояния под мьютексом (Wait всегда в цикле for !cond { c.Wait() }). На практике редко, чаще заменяют закрытием канала.

7. sync.Pool

Кэш временных объектов для снижения нагрузки на GC (буферы, энкодеры). Объекты могут быть удалены в любой GC (с 1.13 — через victim cache переживают один цикл). Не для соединений и не для состояния. Сбрасывайте объект перед Put. Хранить указатели (*bytes.Buffer), а не слайсы — иначе аллокация при упаковке в any.

8. sync/atomic: что знать

  • Типизированные атомики (Go 1.19): atomic.Int64, atomic.Bool, atomic.Pointer[T], atomic.Value.
  • Add, Load, Store, Swap, CompareAndSwap; в Go 1.23 добавлены And/Or.
  • go fix в Go 1.27 умеет переводить старый код на типизированные атомики (модернайзер atomictypes).
var hits atomic.Int64
hits.Add(1)

Атомики быстрее мьютекса для одной переменной, но не делают атомарной группу операций.

9. Что такое модель памяти Go (Go Memory Model)?

Формальные правила happens-before: когда запись в одной горутине гарантированно видна чтению в другой. Гарантии дают:

  • запуск горутины (go f() happens-before начала f);
  • отправка в канал happens-before соответствующего получения; close happens-before получения zero value;
  • для небуферизированного канала получение happens-before завершения отправки;
  • Unlock happens-before следующего Lock;
  • Once.Do(f): завершение f happens-before возврата любого Do;
  • атомики sequentially consistent (с 2022 года это явно в спецификации).

Без этого компилятор и CPU вправе переупорядочивать операции:

var a string; var done bool
go func() { a = "hello"; done = true }()
for !done {}   // может крутиться вечно
print(a)       // может напечатать ""

10. Что такое false sharing?

Две горутины пишут в разные переменные, лежащие в одной кэш-линии (64 байта) → кэш-линия «прыгает» между ядрами. Лечится паддингом _ [56]byte или cpu.CacheLinePad.

11. context + мьютекс: можно ли захватить мьютекс с таймаутом?

Стандартный Mutex — нет (TryLock есть с Go 1.18, но его использование обычно запах дизайна). Альтернатива — семафор на канале: select { case sem <- struct{}{}: case <-ctx.Done(): }.

12. Тестирование конкурентного кода без time.Sleep

Пакет testing/synctest (эксперимент в 1.24, стабилен с Go 1.25): код внутри synctest.Test(t, func(t *testing.T){...}) работает в «пузыре» с фейковым временем — time.Sleep(time.Hour) проходит мгновенно, а synctest.Wait() ждёт, пока все горутины пузыря заблокируются. В Go 1.27 добавлен synctest.Sleep.


Runtime Go: планировщик, стек, escape analysis и сборщик мусора

Вопросы уровня Middle+/Senior. Именно тут отличают «пишу на Go» от «понимаю Go».


Планировщик (scheduler)

1. Модель G-M-P

  • G (goroutine) — горутина: стек, PC, статус.
  • M (machine) — поток ОС.
  • P (processor) — логический процессор: локальная очередь G (до 256), кэш аллокатора mcache. Число P = GOMAXPROCS. M выполняет G, только владея P. Есть глобальная очередь и слот runnext (свежесозданная горутина выполнится следующей — локальность).

2. Work stealing

Если локальная очередь P пуста: проверить глобальную очередь (и её же раз в 61 тик — чтобы не голодала), netpoller, затем украсть половину очереди у случайного P.

3. Что происходит при блокирующем системном вызове?

M уходит в syscall вместе с G, а P отцепляется (hand off) — sysmon отдаёт его другому M, чтобы остальные горутины продолжали работать. Сетевые операции не блокируют M: они идут через netpoller (epoll/kqueue/IOCP) — горутина паркуется, M свободен.

4. Вытеснение (preemption)

  • Кооперативное: проверка в прологе функций (при росте стека).
  • Асинхронное (Go 1.14+): sysmon замечает горутину, работающую > 10 мс, и шлёт сигнал SIGURG потоку → горутина прерывается в безопасной точке. Поэтому for {} больше не вешает планировщик.

5. GOMAXPROCS в контейнерах

До Go 1.25 = число CPU хоста, из-за чего в поде с limits.cpu: 2 на 64-ядерной ноде было 64 P → троттлинг CFS и рост латентности (раньше лечили go.uber.org/automaxprocs). С Go 1.25 рантайм учитывает cgroup CPU limit и периодически обновляет GOMAXPROCS, если лимит изменился.

Стек и память

6. Как растёт стек горутины?

Стартовый размер ~2 КБ (с 1.19 — адаптивный, по среднему использованию). При нехватке — выделяется стек ×2 и копируется целиком (contiguous stacks), указатели на стек корректируются. Поэтому указатели на стек нельзя отдавать в C.

7. Escape analysis

Компилятор решает, где разместить переменную: на стеке (дёшево, освобождается при выходе) или в куче (нагрузка на GC). Переменная «убегает», если:

  • возвращается указатель на неё;
  • сохраняется в глобальную переменную, map, канал, интерфейс (часто);
  • захватывается замыканием, живущим дольше функции;
  • размер неизвестен на этапе компиляции (make([]byte, n)) или слишком велик.
go build -gcflags='-m -m' ./...
# ./main.go:10:2: moved to heap: x

Go 1.25–1.26 научились размещать на стеке больше слайсов с неконстантным размером.

8. Как устроен аллокатор?

Основан на TCMalloc: размерные классы (~68 классов до 32 КБ), mcache (на P, без блокировок) → mcentral (на класс) → mheap (страницы, arena по 64 МБ). Мелкие объекты без указателей (< 16 Б) — tiny allocator. В Go 1.27 — size-specialized malloc, до 30% быстрее мелких аллокаций.

Сборщик мусора

9. Как работает GC в Go?

Concurrent, non-generational, non-moving, tri-color mark-and-sweep с write barrier.

  1. Короткая STW-фаза: включить write barrier.
  2. Конкурентная маркировка: белые (не посещены) → серые (в очереди) → чёрные (обработаны). Горутины, много аллоцирующие, помогают GC (mark assist).
  3. Короткая STW: завершение маркировки.
  4. Конкурентная очистка (sweep) — лениво, при аллокациях. Паузы STW обычно < 1 мс.

10. Что такое Green Tea GC?

Новый алгоритм маркировки (эксперимент в Go 1.25, по умолчанию с Go 1.26): сканирует не отдельные объекты, а целые страницы (spans) мелких объектов — лучше локальность памяти и кэшей, меньше промахов. Снижает накладные расходы GC на 10–40% в реальных программах; на новых CPU с векторными инструкциями — ещё ~10%. Отключение: GOEXPERIMENT=nogreenteagc.

11. GOGC и GOMEMLIMIT

  • GOGC=100 (по умолчанию): следующий GC, когда живая куча вырастет на 100%. Больше — реже GC, больше памяти.
  • GOMEMLIMIT (Go 1.19): мягкий лимит памяти рантайма. GC учащается при приближении. Рекомендация для контейнеров: GOMEMLIMIT ≈ 80–90% лимита пода. GOGC=off + GOMEMLIMIT — GC только у лимита (осторожно: риск death spiral).

12. Как снизить нагрузку на GC?

Меньше аллокаций: предвыделение (make(..., 0, n)), sync.Pool, значения вместо указателей, strings.Builder, избегать any/fmt в горячем пути, структуры без указателей (GC их не сканирует). Смотреть go test -bench . -benchmem, pprof -alloc_space, GODEBUG=gctrace=1.

13. Финализаторы и runtime.AddCleanup

runtime.SetFinalizer — ненадёжен, воскрешает объект, мешает циклам. С Go 1.24 — runtime.AddCleanup(ptr, fn, arg): несколько cleanup на объект, не воскрешает, работает с циклами. Также в 1.24 появился пакет weak (слабые указатели) — для кэшей и интернирования (см. пакет unique, Go 1.23).


Дженерики в Go (1.18 → 1.27): вопросы и ответы


1. Синтаксис и ограничения (constraints)

func Map[T, R any](xs []T, f func(T) R) []R {
    out := make([]R, 0, len(xs))
    for _, x := range xs { out = append(out, f(x)) }
    return out
}

type Number interface { ~int | ~int64 | ~float64 }
func Sum[T Number](xs ...T) (s T) { for _, x := range xs { s += x }; return }
  • any — любой тип; comparable — поддерживает == (с Go 1.20 включает интерфейсы, которые могут паниковать при сравнении).
  • ~int — любой тип с underlying int (например, type UserID int).
  • cmp.Ordered (Go 1.21) — всё, что поддерживает < <= > >=.

2. Как дженерики реализованы? Есть ли оверхед?

GC shape stenciling + словари. Код генерируется на каждую «форму» (GC shape): все указательные типы делят одну реализацию, методы вызываются через словарь → косвенный вызов, иногда медленнее интерфейсов и мешает инлайнингу. Для значимых типов (int, float64) — отдельная специализированная копия, быстро. Вывод для собеса: дженерики — для структур данных и алгоритмов (контейнеры, slices, maps), а не замена интерфейсам в бизнес-логике.

3. Дженерики или интерфейсы?

  • Интерфейс — когда важно поведение (методы), а конкретный тип не важен.
  • Дженерик — когда одна и та же логика для разных типов и важно сохранить тип (без any и type assertion), например Max[T], Cache[K, V].

4. Что нельзя делать с дженериками?

  • Методы с собственными параметрами типа — можно с Go 1.27 (см. ниже). Но в интерфейсах методы с параметрами типа по-прежнему запрещены.
  • Специализация (разная реализация для конкретного T) — нет, только type switch по any(v).
  • Параметры типа в type switch/case напрямую, variadic type parameters — нет.

5. Go 1.27: generic-методы

Методы теперь могут объявлять свои параметры типа:

type Stream[T any] struct{ items []T }

func (s Stream[T]) Map[R any](f func(T) R) Stream[R] { // Go 1.27+
    out := make([]R, len(s.items))
    for i, v := range s.items { out[i] = f(v) }
    return Stream[R]{out}
}

Пример в стандартной библиотеке — (*rand.Rand).N[Int intType](Int) Int в math/rand/v2. Ограничение: такие методы не участвуют в реализации интерфейсов.

6. Go 1.26: самоссылающиеся ограничения

type Adder[A Adder[A]] interface { Add(A) A }

func SumAll[A Adder[A]](xs ...A) A { /* ... */ }

Раньше тип не мог ссылаться на себя в своём списке параметров типа.

7. Параметризованные алиасы (Go 1.24)

type Set[T comparable] = map[T]struct{}

8. Итераторы (Go 1.23) и дженерики

func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range seq {
            if pred(v) && !yield(v) { return }
        }
    }
}
for v := range Filter(slices.Values(xs), isEven) { ... }

iter.Seq[V] = func(yield func(V) bool), iter.Seq2[K,V]. Важно проверять результат yield — иначе panic при break в цикле потребителя.

9. Инференс типов

Компилятор выводит параметры по аргументам: Map(xs, strconv.Itoa). С Go 1.21 — и по типу присваивания/возвращаемому значению; в Go 1.27 инференс работает во всех контекстах, где дженерик-функция присваивается переменной или конвертируется в тип функции.

10. Популярная задача: generic Filter, Reduce, Set, LRU

Решения: lrucache, intersect, pipeline.Map.


context.Context в Go: отмена, таймауты, значения


1. Зачем нужен context?

Передача по цепочке вызовов (и между горутинами) сигнала отмены, дедлайна и request-scoped значений (trace ID, user ID). Контекст — дерево: отмена родителя отменяет всех потомков, но не наоборот.

2. Интерфейс

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error          // nil, Canceled или DeadlineExceeded
    Value(key any) any
}

3. Конструкторы — все, что нужно знать

Функция Версия Назначение
Background() / TODO() 1.7 корень / «ещё не решили»
WithCancel 1.7 ручная отмена
WithTimeout / WithDeadline 1.7 по времени
WithValue 1.7 значение
WithCancelCause + Cause(ctx) 1.20 отмена с причиной
WithTimeoutCause / WithDeadlineCause 1.21 таймаут с причиной
AfterFunc(ctx, f) 1.21 вызвать f после отмены
WithoutCancel(ctx) 1.21 значения сохраняются, отмена — нет (фоновая работа после ответа)
signal.NotifyContext 1.16 отмена по сигналу ОС; с 1.26 причина — сигнал
t.Context() / b.Context() 1.24 контекст теста, отменяется перед Cleanup

4. Почему обязательно вызывать cancel()?

Иначе дочерний контекст (и его таймер) живёт до отмены родителя → утечка. go vet предупреждает (lostcancel). Идиома: ctx, cancel := context.WithTimeout(...); defer cancel().

5. Правила использования

  • Первый параметр функции: func Do(ctx context.Context, ...).
  • Не хранить в структурах (исключение — структуры, представляющие одну операцию).
  • Не передавать nil — используйте context.TODO().
  • Ключи WithValue — свой неэкспортируемый тип, чтобы не было коллизий:
type ctxKey struct{}
ctx = context.WithValue(ctx, ctxKey{}, userID)
  • В Value — только request-scoped данные, не параметры функций и не зависимости (логгер/БД — спорно; лучше явно).

6. Как работает Value и почему он медленный?

Каждый WithValue — новый узел связного списка; поиск идёт от листа к корню, O(глубина). Не кладите туда десятки значений.

7. Как отмена доходит до HTTP-клиента и БД?

http.NewRequestWithContext(ctx, ...), db.QueryContext(ctx, ...), grpc — принимают ctx и прерывают операцию. В net/http сервере r.Context() отменяется при разрыве клиентского соединения или завершении ServeHTTP.

8. Что выведет?

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
child, cancelChild := context.WithTimeout(ctx, time.Hour)
defer cancelChild()
cancel()
fmt.Println(child.Err()) // context canceled — дедлайн потомка не может быть позже родительского

9. Как проверить отмену в горячем цикле?

for i, item := range items {
    if i%1000 == 0 {
        if err := ctx.Err(); err != nil { return err }
    }
    process(item)
}

Или неблокирующий select { case <-ctx.Done(): return ctx.Err(); default: }.


Тестирование, бенчмарки и профилирование Go


1. Table-driven тесты

func TestAbs(t *testing.T) {
    tests := []struct{ name string; in, want int }{
        {"positive", 5, 5}, {"negative", -5, 5}, {"zero", 0, 0},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            if got := Abs(tt.in); got != tt.want {
                t.Errorf("Abs(%d) = %d, want %d", tt.in, got, tt.want)
            }
        })
    }
}

С Go 1.22 tt := tt перед t.Parallel() больше не нужен.

2. t.Error vs t.Fatal, t.Helper, t.Cleanup, t.TempDir, t.Setenv, t.Context

  • Error — отметить провал и продолжить; Fatal — провал и runtime.Goexit (нельзя вызывать из других горутин!).
  • t.Helper() — строки ошибок указывают на вызывающего.
  • t.Cleanup(f) — как defer, но для теста и подтестов.
  • t.Context() (1.24), t.Chdir() (1.24), t.ArtifactDir() (1.26), t.Output() (1.25).

3. Бенчмарки

func BenchmarkParse(b *testing.B) {
    for b.Loop() {        // Go 1.24+: не нужно b.ResetTimer, компилятор не выкинет тело
        Parse(input)
    }
}

go test -bench=. -benchmem -count=10 | tee new.txt и сравнение через benchstat old.txt new.txt.

4. Моки

Интерфейс на стороне потребителя + ручной фейк или генерация: go.uber.org/mock (mockgen, форк gomock), mockery, minimock (популярен в РФ). Для HTTP — httptest.NewServer, httptest.NewRecorder; в Go 1.27 — httptest.NewTestServer (in-memory). Для БД — testcontainers-go или sqlmock.

5. Фаззинг (Go 1.18+)

func FuzzReverse(f *testing.F) {
    f.Add("hello")
    f.Fuzz(func(t *testing.T, s string) {
        if Reverse(Reverse(s)) != s { t.Fatal(s) }
    })
}

go test -fuzz=FuzzReverse -fuzztime=30s.

6. testing/synctest (Go 1.25)

Детерминированное тестирование кода с time и горутинами без реальных Sleep. См. раздел про sync.

7. Покрытие

go test -coverprofile=c.out ./... && go tool cover -html=c.out. Для интеграционных тестов — go build -cover + GOCOVERDIR.

8. pprof: какие профили бывают?

Профиль Что показывает
cpu где тратится процессорное время (семплирование 100 Гц)
heap inuse_space (сейчас в памяти) / alloc_space (всего выделено)
allocs все аллокации
goroutine стеки всех горутин
goroutineleak утёкшие горутины (Go 1.26 эксп., 1.27 по умолчанию)
block ожидание на каналах/мьютексах (включить SetBlockProfileRate)
mutex конкуренция за мьютексы (SetMutexProfileFraction)
import _ "net/http/pprof" // регистрирует /debug/pprof/* в DefaultServeMux — не выставляйте наружу!
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30

С Go 1.26 веб-интерфейс pprof по умолчанию открывает flame graph.

9. go tool trace и Flight Recorder

Трассировка показывает работу планировщика, GC, блокировки по времени. Go 1.25: runtime/trace.FlightRecorder — держит последние секунды трейса в кольцевом буфере и сохраняет их, когда случилась аномалия (например, медленный запрос).

10. PGO (Profile-Guided Optimization)

Положите CPU-профиль продакшена как default.pgo в пакет main — go build использует его для инлайнинга и девиртуализации. Типичный выигрыш — 2–14%.

11. Линтеры

go vet (обязателен), staticcheck, golangci-lint (агрегатор), govulncheck (уязвимости зависимостей), go fix — с Go 1.26 применяет модернайзеры (переводит код на min/max, slices, range int, wg.Go и т.д.).


Go Backend на собеседовании: HTTP, gRPC, базы данных, брокеры сообщений


HTTP

1. Как устроен net/http сервер?

ListenAndServe → Accept в цикле → горутина на каждое соединение → чтение запроса → Handler.ServeHTTP(w, r). Роутинг — http.ServeMux.

2. Что умеет ServeMux с Go 1.22?

Методы и wildcard-параметры — во многих проектах больше не нужен chi/gorilla:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    ...
})
mux.HandleFunc("POST /users/", createUser)
mux.HandleFunc("GET /files/{path...}", serveFile)

3. Middleware

func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        slog.Info("req", "method", r.Method, "path", r.URL.Path, "dur", time.Since(start))
    })
}

Порядок: recover → request ID → логирование → метрики → auth → rate limit → handler.

4. Таймауты сервера и клиента (частая ошибка в проде)

  • Сервер: ReadHeaderTimeout, ReadTimeout, WriteTimeout, IdleTimeout. Без них — Slowloris.
  • Клиент: http.DefaultClient без таймаута → зависшие запросы навсегда. Задавайте Client{Timeout: ...} или контекст.
  • Обязательно defer resp.Body.Close() и дочитывать тело (io.Copy(io.Discard, resp.Body)), иначе соединение не вернётся в пул keep-alive.
  • Transport.MaxIdleConnsPerHost по умолчанию 2 — узкое место при высоком RPS к одному хосту.

5. Graceful shutdown

См. задачу gracefulshutdown.

6. Логирование

log/slog (Go 1.21) — структурированный логгер в стандартной библиотеке; slog.NewMultiHandler (Go 1.26). Раньше — zap, zerolog.

7. JSON

encoding/json — через рефлексию, медленный. Опции тегов: omitempty, omitzero (1.24), string, -. Go 1.27: encoding/json/v2 и encoding/json/jsontext — быстрее, строже (отклоняет невалидный UTF-8, дубликаты ключей), поддерживает стриминг. Альтернативы: easyjson, sonic, goccy/go-json.

gRPC

8. gRPC vs REST

gRPC REST/JSON
Транспорт HTTP/2, мультиплексирование HTTP/1.1 или 2
Формат Protobuf (бинарный, схема) JSON (текст)
Стриминг unary, server, client, bidi SSE/WebSocket отдельно
Контракт .proto + кодогенерация OpenAPI (опционально)
Браузер нужен gRPC-Web / Connect нативно

9. Что спрашивают про gRPC

  • Interceptors (unary/stream) — аналог middleware.
  • Дедлайны передаются в метаданных и превращаются в ctx на сервере.
  • Коды ошибок status.Error(codes.NotFound, ...).
  • Балансировка: HTTP/2 держит одно долгое соединение → L4-балансировщик распределяет плохо; нужен client-side LB (round_robin) или L7 (Envoy).
  • Обратная совместимость protobuf: нельзя менять номера полей, удалённые — reserved.

Базы данных

10. database/sql: пул соединений

sql.DB — пул, потокобезопасен, создаётся один раз. Настройки: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime, SetConnMaxIdleTime. Для Postgres в Go-мире стандарт — jackc/pgx/v5 (+ pgxpool).

11. Типичные утечки

Не закрыли rows → соединение не вернётся в пул: всегда defer rows.Close() и проверка rows.Err(). QueryRow закрывается сам после Scan.

12. Транзакции и уровни изоляции

tx, err := db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead})
defer tx.Rollback() // no-op после Commit
...
return tx.Commit()
Уровень Dirty read Non-repeatable Phantom Postgres
Read Uncommitted возможно возможно возможно ведёт себя как RC
Read Committed — возможно возможно по умолчанию
Repeatable Read — — возможно по стандарту в PG нет фантомов (snapshot)
Serializable — — — SSI, нужно ретраить 40001

13. Индексы — что спросят

B-tree (по умолчанию, =, диапазоны, сортировка), Hash, GIN (jsonb, массивы, полнотекст), GiST/BRIN. Составной индекс и правило левого префикса. Покрывающий индекс (INCLUDE). EXPLAIN (ANALYZE, BUFFERS). Почему индекс не используется: функция над колонкой, низкая селективность, LIKE '%x', несовпадение типов.

14. N+1, SQL-инъекции, ORM

  • N+1 — запрос в цикле → WHERE id = ANY($1) или JOIN.
  • Инъекции — только плейсхолдеры ($1), никакого fmt.Sprintf в SQL.
  • В Go чаще sqlc (генерация из SQL), squirrel/goqu, реже GORM/ent.

15. Миграции

goose, golang-migrate, atlas. Правило zero-downtime: расширяем → деплоим код → сужаем (expand/contract), CREATE INDEX CONCURRENTLY.

Брокеры и кэш

16. Kafka: что нужно знать Go-разработчику

Топик → партиции → упорядоченность только внутри партиции (ключ сообщения определяет партицию). Consumer group: одна партиция — один консьюмер группы. Offset commit после обработки → at-least-once → идемпотентные обработчики. Exactly-once — транзакции Kafka или дедупликация на стороне получателя. Клиенты: segmentio/kafka-go, twmb/franz-go, IBM/sarama, confluent-kafka-go.

17. Outbox pattern

Как атомарно записать в БД и отправить событие? Пишем событие в таблицу outbox в той же транзакции, отдельный процесс читает и публикует в Kafka (или CDC через Debezium).

18. Redis: что спрашивают

Типы данных, TTL, стратегии кэширования (cache-aside, write-through, write-behind), инвалидация, cache stampede (singleflight), распределённый лок (SET NX PX + fencing token; Redlock и его критика), Lua-скрипты для атомарности.


Архитектура Go-сервисов и паттерны проектирования


1. Структура проекта

Официальный гайд: go.dev/doc/modules/layout. Популярная (неофициальная) схема:

cmd/app/main.go        # точка входа, сборка зависимостей (wiring)
internal/              # код, недоступный извне модуля
  domain/              # сущности и бизнес-правила
  service/ (usecase)   # сценарии
  repository/ (storage)# работа с БД
  transport/http, grpc # хендлеры
pkg/                   # публичные библиотеки (если нужны)
migrations/, api/ (proto, openapi)

Не тащите pkg/ и глубокую вложенность в маленький сервис. «Clean architecture» в Go — это прежде всего направление зависимостей внутрь и интерфейсы на стороне потребителя.

2. SOLID в Go

  • S — маленькие пакеты с одной ответственностью.
  • O — расширение через интерфейсы и композицию.
  • L — любая реализация интерфейса взаимозаменяема.
  • I — маленькие интерфейсы (io.Reader, io.Writer) → io.ReadWriter композицией.
  • D — сервис зависит от интерфейса UserRepo, объявленного в пакете сервиса, а не от *PostgresRepo.

3. Dependency Injection

В Go обычно ручная сборка в main (конструкторы NewService(repo, logger)). Для больших проектов — google/wire (кодогенерация) или uber-go/fx (рантайм).

4. Паттерны, которые спрашивают в Go-контексте

Паттерн В Go
Singleton sync.OnceValue
Factory функция NewX(...)
Functional Options NewServer(addr, WithTimeout(5*time.Second))
Decorator / Middleware func(http.Handler) http.Handler
Adapter http.HandlerFunc — функция как интерфейс
Strategy интерфейс или функция-параметр
Observer каналы / pub-sub
Circuit Breaker sony/gobreaker
Retry с backoff + jitter экспоненциальная задержка + случайность

5. Микросервисы: что обязательно знать

  • Идемпотентность (idempotency key), ретраи только для идемпотентных операций.
  • Таймауты и дедлайны по всей цепочке, circuit breaker, bulkhead.
  • Распределённые транзакции: Saga (оркестрация/хореография) вместо 2PC; Outbox.
  • Observability: логи (slog), метрики (Prometheus: RED/USE), трейсы (OpenTelemetry).
  • Health checks: liveness vs readiness.
  • Конфигурация через env (12-factor).

6. Прометеус-метрики: какие типы?

Counter (только растёт), Gauge (вверх-вниз), Histogram (бакеты, агрегируется между инстансами → p99 через histogram_quantile), Summary (квантили на клиенте, не агрегируется). Не кладите user ID в лейблы — взрыв кардинальности.


Продвинутый Go: внутренности рантайма, компилятор, unsafe и производительность (Senior+)

Вопросы для Senior/Staff и команд, где Go — основной язык (Яндекс, Ozon, Авито, VK, Wildberries, Kaspersky, биржи и финтех). Здесь не проверяют «знаете ли вы Go», здесь проверяют, понимаете ли вы, что происходит под капотом и можете ли объяснить поведение программы в проде.


Внутренности рантайма

1. Как устроен канал внутри (runtime.hchan)?

type hchan struct {
	qcount   uint           // элементов в буфере
	dataqsiz uint           // размер кольцевого буфера (cap)
	buf      unsafe.Pointer // кольцевой буфер
	elemsize uint16
	closed   uint32
	elemtype *_type
	sendx    uint  // индекс записи в buf
	recvx    uint  // индекс чтения из buf
	recvq    waitq // очередь ждущих получателей (sudog)
	sendq    waitq // очередь ждущих отправителей (sudog)
	lock     mutex // внутренний runtime-мьютекс
}
  • make(chan T, n) выделяет hchan + буфер в куче; канал — это указатель на hchan.
  • Отправка: если в recvq есть ждущий получатель — значение копируется напрямую в его стек (sendDirect) и горутина будится, буфер не трогается. Иначе, если есть место в буфере, — пишем в buf[sendx]. Иначе — создаём sudog, кладём в sendq и паркуемся (gopark).
  • Получение симметрично; если есть ждущий отправитель и буфер полон — берём из головы буфера, а значение отправителя кладём в хвост (сохраняем FIFO).
  • close: будит всех из recvq (получат zero value, ok == false) и из sendq (они запаникуют).
  • Любая операция берёт lock — поэтому канал не быстрее мьютекса; это инструмент синхронизации и передачи владения, а не lock-free очередь.

2. Как работает select внутри?

Компилятор оптимизирует частные случаи: select {} — вечная блокировка; один case — обычная операция с каналом; один case + default — неблокирующие selectnbsend/selectnbrecv. Общий случай — runtime.selectgo:

  1. pollorder — случайная перестановка кейсов (поэтому выбор среди готовых случаен и нет голодания).
  2. lockorder — кейсы, отсортированные по адресу канала: все каналы блокируются в едином порядке, чтобы два select не устроили deadlock друг другу.
  3. Проход по pollorder: если что-то готово — выполняем и выходим. Есть default — выходим через него.
  4. Иначе горутина ставит свой sudog в очереди всех каналов и паркуется. Когда один канал её будит, она снимает sudog из остальных очередей.

Вывод для собеса: select на N каналах — это O(N) блокировок на каждый вызов; большой select в горячем цикле дорог.

3. Что изменилось в таймерах в Go 1.23?

  • Таймеры хранятся в 4-арной куче на каждом P (с Go 1.14; раньше была глобальная куча и отдельная горутина).
  • С Go 1.23 (если в go.mod указан go 1.23+):
    • таймеры и тикеры, на которые никто не ссылается, собираются GC даже без Stop() — time.After в цикле больше не течёт до срабатывания;
    • канал таймера стал синхронным (unbuffered): после Stop()/Reset() из t.C гарантированно не придёт «протухшее» значение — старый идиом if !t.Stop() { <-t.C } больше не нужен.
  • Вернуть старое поведение: GODEBUG=asynctimerchan=1.

4. Что делает sysmon?

Системный монитор — отдельный поток без P (не участвует в планировании Go-кода), просыпается каждые 20 мкс…10 мс (засыпает дольше, если всё спокойно):

  • отбирает P у потоков, застрявших в syscall/cgo, и отдаёт другому M;
  • вытесняет горутины, работающие дольше 10 мс (сигнал SIGURG, асинхронное вытеснение);
  • опрашивает netpoller, если его давно никто не проверял;
  • форсирует GC, если его не было 2 минуты;
  • будит scavenger, возвращающий неиспользуемую память ОС.

5. Что такое hybrid write barrier и зачем он нужен?

Во время конкурентной маркировки программа (mutator) меняет указатели, и GC может «потерять» живой объект: чёрный объект начинает ссылаться на белый, а единственный серый путь к белому удаляется. Write barrier перехватывает запись указателя и красит объекты в серый.

  • До Go 1.8 — барьер Дейкстры (insertion), но стеки горутин им не покрывались → в конце маркировки нужен был STW с повторным сканированием всех стеков (десятки мс на больших программах).
  • С Go 1.8 — гибридный барьер (Yuasa deletion + Dijkstra insertion): красится и старое, и новое значение указателя. Стек каждой горутины сканируется один раз и больше не пересканируется → STW-паузы стали субмиллисекундными.
  • Барьер включён только во время маркировки; вне её запись указателя — просто проверка флага.

6. Как устроена map на Swiss Tables (Go 1.24+) — глубже

  • Map = directory указателей на таблицы (до 1024 слотов каждая); таблица = массив групп по 8 слотов + 8-байтовое контрольное слово (по байту на слот: пусто / удалён / 7 младших бит хэша — H2).
  • Хэш делится на H1 (старшие 57 бит — выбор группы и пробирование) и H2 (7 бит — в контрольном слове).
  • Поиск: по H1 находим группу, одним сравнением 8 байт контрольного слова (SIMD или SWAR-трюк на обычных регистрах) находим слоты-кандидаты с совпадающим H2, и только их ключи сравниваем полностью. Дальше — квадратичное пробирование по группам.
  • Рост: растёт только переполненная таблица (расширяемое хэширование): таблица делится на две, directory удваивается при необходимости. Нет больших «стоп-мир» рехэшей и нет overflow-бакетов.
  • Маленькие map (≤ 8 элементов) — одна группа без directory и таблиц.
  • Итерация остаётся рандомизированной и корректной при вставках во время range (итератор держит ссылку на старую таблицу).

7. Как работают range-over-func итераторы и iter.Pull?

  • for x := range seq { body } компилятор переписывает в вызов seq(func(x T) bool { body; return true }). break/return внутри тела превращаются в return false + флаги состояния; defer в теле цикла привязывается к внешней функции.
  • Если итератор игнорирует false от yield и зовёт его снова — рантайм паникует: range function continued iteration after function for loop body returned false.
  • iter.Pull(seq) превращает push-итератор в pull (next(), stop()). Реализован на корутинах рантайма (runtime.coroswitch): это не горутина с каналами, а прямое переключение между двумя стеками без планировщика — на порядок дешевле канального решения. Обязательно вызывайте stop(), иначе корутина и её ресурсы останутся жить.

8. Как в time.Time устроено монотонное время?

time.Now() возвращает wall clock + монотонное показание. t2.Sub(t1), time.Since, Before/After используют монотонную часть, если она есть у обоих, — поэтому перевод системных часов (NTP, ручная правка) не ломает измерение интервалов. Монотонная часть отбрасывается при t.Round(0), сериализации, t.In(loc)/UTC() — не сохраняйте time.Time в БД и потом не считайте по нему таймауты. Сравнивать time.Time через == нельзя (сравнивается и локация, и монотоника) — используйте t.Equal(u).


Компилятор и оптимизации

9. Как работает инлайнинг и как его контролировать?

  • Бюджет — 80 узлов AST на функцию; с PGO для горячих вызовов бюджет поднимается до 2000.
  • Mid-stack inlining (с 1.12): инлайнятся и не-листовые функции.
  • Мешают инлайнингу (Go 1.24): defer, recover, go-выражения, слишком большой размер. Циклы, select, замыкания — уже не мешают.
  • Инлайнинг важен не только из-за стоимости вызова: он открывает escape analysis — объект, созданный во встроенной функции, может остаться на стеке вызывающей.
go build -gcflags='-m' ./...          # can inline / inlining call to
go build -gcflags='-m=2' ./...        # почему НЕ заинлайнено (cost 95 exceeds budget 80)

//go:noinline — запретить (нужно в бенчмарках, чтобы компилятор не выкинул код).

10. Что такое bounds check elimination (BCE)?

Каждое s[i] компилируется с проверкой i < len(s) (иначе паника). Компилятор убирает проверки, если может доказать безопасность. Классический приём — подсказка заранее:

func readUint32(b []byte) uint32 {
	_ = b[3] // одна проверка вместо четырёх
	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
}

Увидеть оставшиеся проверки: go build -gcflags='-d=ssa/check_bce/debug=1'. Так написан encoding/binary.

11. Как работает PGO (Profile-Guided Optimization)?

Кладёте CPU-профиль из прода в default.pgo рядом с main-пакетом — go build использует его автоматически (с Go 1.21). Что делает компилятор:

  • агрессивнее инлайнит горячие вызовы;
  • девиртуализирует горячие интерфейсные вызовы: if r, ok := x.(*os.File); ok { r.Read() } else { x.Read() } — прямой вызов можно заинлайнить;
  • раскладывает горячие блоки кода. Типичный выигрыш — 2–14% CPU. Профиль не обязан точно соответствовать коду — PGO устойчив к изменениям. Цикл: собрать → задеплоить → снять профиль → закоммитить default.pgo.

12. Какие директивы компилятора нужно знать?

Директива Что делает
//go:build linux && amd64 условия сборки (заменила // +build)
//go:embed static/* встроить файлы в бинарник (embed.FS)
//go:generate stringer -type=Color команда для go generate (не запускается при go build)
//go:noinline запретить инлайнинг
//go:nosplit не вставлять проверку роста стека (рантайм, обработчики сигналов)
//go:noescape для функций без тела (asm): аргументы не убегают
//go:linkname local pkg.name доступ к неэкспортируемому символу другого пакета. С Go 1.23 линкер запрещает «вытягивать» внутренности рантайма, если они не помечены как разрешённые (-ldflags=-checklinkname=0 — отключить, но это путь к поломке при обновлении)
//go:wasmexport экспорт функции из Wasm-модуля (Go 1.24)

13. Как посмотреть, во что скомпилировался код?

  • go build -gcflags=-S — ассемблер Go (синтаксис Plan 9: MOVQ src, dst, псевдорегистры SB, FP, SP).
  • go tool objdump -s 'main.hot' ./bin — дизассемблирование бинарника.
  • GOSSAFUNC=hot go build — HTML со всеми проходами SSA.
  • Compiler Explorer (godbolt.org) — быстро сравнить версии Go.
  • ABI: с Go 1.17 аргументы передаются через регистры (ABIInternal) — ~5% прироста; ассемблерные функции используют стековый ABI0 через обёртки.

unsafe и reflect

14. Какие правила у unsafe.Pointer?

Допустимы только шаблоны из документации пакета unsafe, главное:

  1. *T1 → unsafe.Pointer → *T2 (если T2 не больше T1 и раскладка совместима).
  2. unsafe.Pointer → uintptr — только для печати/сравнения; обратно превращать нельзя.
  3. Арифметика unsafe.Pointer(uintptr(p) + off) — в одном выражении. Нельзя сохранить uintptr в переменную: для GC это просто число, объект могут собрать или (при росте стека) переместить. Лучше unsafe.Add(p, off) (Go 1.17).
  4. uintptr в аргументах syscall.Syscall — компилятор держит объект живым до конца вызова.
  5. Результат reflect.Value.Pointer()/UnsafeAddr() — сразу превращать в unsafe.Pointer.

Проверка: go test -race и -gcflags=all=-d=checkptr ловят часть нарушений в рантайме. go vet ловит «possible misuse of unsafe.Pointer».

15. Как сконвертировать []byte ↔ string без копирования?

// Go 1.20+: вместо устаревших reflect.StringHeader/SliceHeader
func b2s(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) }
func s2b(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) }

Условия безопасности: после b2s нельзя менять b (строки неизменяемы — сломаются map-ключи, интернирование); результат s2b нельзя изменять вообще (строковый литерал лежит в read-only памяти → SIGSEGV, это не panic). Часто и не нужно: компилятор сам не копирует в m[string(b)], string(b) == "x", switch string(b), for i, r := range string(b) и конкатенации, результат которой сразу сравнивается.

16. Насколько дорог reflect и когда он оправдан?

  • reflect.ValueOf(x) почти всегда вызывает escape в кучу; вызовы методов через Value.Call — в десятки раз медленнее прямых; каждый доступ проверяет флаги (addressable, exported).
  • Изменить значение можно только через адресуемый Value: reflect.ValueOf(&x).Elem().SetInt(5); неэкспортируемые поля — CanSet() == false.
  • Оправдан: сериализация (encoding/json, ORM), DI-контейнеры, валидаторы, тесты (reflect.DeepEqual, лучше go-cmp). Приём ускорения — кэшировать разбор типа (sync.Map[reflect.Type]*plan), как делает encoding/json.
  • Альтернативы: дженерики, кодогенерация (easyjson, sqlc, protoc-gen-go).
  • Полезное новое: reflect.TypeFor[T]() (1.22), Value.Seq/Seq2 (1.23), Type.CanSeq.

17. Чем опасен cgo?

  • Каждый вызов C — переход на системный стек и оформление как syscall (P может быть отобран sysmon): десятки наносекунд против ~1 нс у Go-вызова (в Go 1.26 оверхед снизили ~на 30%, но порядок тот же). Вызовы C в горячем цикле — антипаттерн: батчуйте.
  • Долгий C-вызов занимает поток ОС; тысячи параллельных C-вызовов = тысячи потоков.
  • Правила указателей: в C можно передать указатель на Go-память, только если она сама не содержит Go-указателей, и C не может хранить его после возврата. Нарушения ловит GODEBUG=cgocheck=1 (по умолчанию). Для долгоживущих указателей — runtime.Pinner (1.21).
  • Нельзя кросс-компилировать без C-тулчейна, бинарник перестаёт быть статическим, pprof/race хуже видят C-код. Поэтому CGO_ENABLED=0 — дефолт для контейнеров (не забудьте про net/os/user: с cgo они используют libc-резолвер).

Производительность и память в проде

18. Какие бывают утечки памяти в Go (при наличии GC)?

  1. Горутины, заблокированные навсегда (самая частая) — держат стек и всё, на что ссылаются.
  2. Подслайс/подстрока большого буфера: small := big[:10] держит весь big. Лечить bytes.Clone, strings.Clone, slices.Clone.
  3. Map не сжимается после delete — периодически пересоздавать.
  4. Кэши без лимита и TTL, глобальные map «на всякий случай».
  5. time.Ticker без Stop и time.After в цикле — до Go 1.23 (см. вопрос про таймеры).
  6. Циклы с SetFinalizer — никогда не освобождаются (используйте runtime.AddCleanup).
  7. Память C через cgo — GC о ней не знает.
  8. sync.Pool с объектами огромной ёмкости: один большой буфер возвращается в пул и живёт вечно — ограничивайте cap перед Put. Диагностика: pprof -inuse_space дважды с интервалом и -diff_base, профиль горутин, runtime/metrics.

19. Как писать код без лишних аллокаций в горячем пути?

  • Предвыделять: make([]T, 0, n), strings.Builder.Grow, bytes.Buffer.Grow.
  • strconv.AppendInt(buf, x, 10) вместо fmt.Sprintf и strconv.Itoa + конкатенации; fmt почти всегда аллоцирует (аргументы упаковываются в any).
  • Не упаковывать в интерфейс: any, error, логгеры с ...any (в slog — slog.Int, LogAttrs).
  • Переиспользовать буферы: buf = buf[:0], clear(m) вместо новой map.
  • sync.Pool хранит указатели: pool.Put(&buf) (*[]byte), иначе сам Put аллоцирует при упаковке слайса в any (линтер SA6002).
  • Замыкания, захватывающие переменные и передающиеся дальше, → аллокации. Методы-значения (f := obj.Method) — тоже.
  • Проверять: go test -bench . -benchmem, testing.AllocsPerRun, -gcflags=-m.

20. runtime.ReadMemStats vs runtime/metrics

ReadMemStats делает stop-the-world — вызывать его раз в секунду в проде плохо. runtime/metrics (Go 1.16) читает метрики без STW и даёт больше: гистограммы пауз GC (/gc/pauses:seconds), задержку планировщика (/sched/latencies:seconds — сколько горутины ждут в очереди P, лучший индикатор нехватки CPU и троттлинга), число горутин, лимиты. prometheus/client_golang с новыми версиями использует именно его.

21. Как расследовать рост латентности p99, если CPU профиль «нормальный»?

CPU-профиль показывает только on-CPU время. Латентность часто уходит в off-CPU:

  • block и mutex профили (runtime.SetBlockProfileRate, SetMutexProfileFraction) — ожидание каналов и блокировок;
  • go tool trace — видно, когда горутина была runnable, но не получила P; паузы GC и mark assist; syscall'ы;
  • Flight Recorder (runtime/trace.FlightRecorder, Go 1.25) — кольцевой буфер трейса в памяти, снимаете последние секунды после того, как заметили всплеск;
  • /sched/latencies:seconds и метрики троттлинга cgroup (nr_throttled);
  • mark assist: если аллоцируете быстрее, чем GC успевает, ваши горутины сами помогают GC — это видно в trace и в CPU-профиле как gcAssistAlloc.

22. Что такое unique и weak, и где они нужны?

  • unique.Make(v) (Go 1.23) — интернирование: возвращает unique.Handle[T]; одинаковые значения дают одинаковые хэндлы, сравнение хэндлов — сравнение указателей (O(1) вместо сравнения длинных строк). Используется в net/netip для зон IPv6. Незадействованные значения собирает GC.
  • weak.Pointer[T] (Go 1.24) — слабая ссылка: не держит объект живым; p.Value() вернёт nil, если объект собран. Применение — кэши, которые не мешают GC освобождать память, канонизирующие map. Вместе с runtime.AddCleanup — удалять запись из кэша, когда объект умер.

23. Как устроен lock-free алгоритм на Go и что такое ABA?

Основа — CAS-цикл: прочитать, вычислить новое значение, CompareAndSwap; если кто-то успел изменить — повторить. Инструменты — atomic.Int64, atomic.Pointer[T] (Go 1.19). ABA-проблема: поток прочитал A, другой поменял A→B→A (и освободил/переиспользовал память узла), CAS первого проходит, хотя структура уже другая. В C/C++ лечат tagged pointers и hazard pointers. В Go благодаря GC ABA на указателях невозможна: пока вы держите указатель на узел, его память не будет переиспользована. Пример — lock-free стек. Когда lock-free не нужен: почти всегда. Мьютекс без конкуренции стоит ~10–20 нс; lock-free выигрывает только при очень высокой конкуренции и маленькой критической секции — и его сложно доказать корректным.

24. Copy-on-write конфиг: как обновлять настройки без блокировок на чтении?

var cfg atomic.Pointer[Config]

func Get() *Config { return cfg.Load() }       // читатели: один атомарный load, без блокировок

func Update(mut func(*Config)) {                // писатели: редко, сериализованы мьютексом
	mu.Lock()
	defer mu.Unlock()
	next := *cfg.Load()                          // копия (глубже, если внутри map/слайсы!)
	mut(&next)
	cfg.Store(&next)
}

Главное условие: опубликованный *Config никто не меняет после Store. Идеально для feature flags, таблиц маршрутизации, сертификатов.


Модули, сборка, безопасность

25. Как Go выбирает версии зависимостей (MVS)?

Minimal Version Selection: для каждого модуля берётся максимальная из минимально требуемых версий в графе — не «самая свежая». Сборка детерминирована без lock-файла; go.sum хранит только хэши для проверки целостности (через sum.golang.org).

  • Мажорная версия ≥ 2 — другой путь модуля: example.com/lib/v2.
  • retract в go.mod — пометить свою версию как битую.
  • replace — подменить модуль (локальная отладка); работает только в главном модуле.
  • Приватные модули: GOPRIVATE=gitlab.company.ru/* (не ходить в прокси и sumdb), GOPROXY, GOFLAGS=-mod=vendor.
  • tool-директива в go.mod (Go 1.24) — версионируемые инструменты разработки (go get -tool, go tool stringer) вместо tools.go.

26. Как собрать минимальный воспроизводимый бинарник для прода?

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
  go build -trimpath -ldflags="-s -w -X main.version=$(git describe --tags)" -o app ./cmd/app

-trimpath — убрать локальные пути (воспроизводимость), -s -w — без таблицы символов и DWARF (меньше размер, но хуже отладка), -X — подставить версию. Информация о сборке и VCS доступна в рантайме через debug.ReadBuildInfo() и go version -m ./app. Образ — FROM scratch или distroless/static (не забудьте CA-сертификаты и tzdata, либо import _ "time/tzdata").

27. Что спросят про безопасность в Go?

  • math/rand (и math/rand/v2) — не для токенов и паролей; только crypto/rand (с Go 1.24 crypto/rand.Text() для случайных строк).
  • Сравнение секретов — subtle.ConstantTimeCompare, иначе timing attack.
  • SQL — только плейсхолдеры ($1), никогда fmt.Sprintf.
  • html/template экранирует контекстно, text/template — нет (XSS).
  • os/exec.Command(name, args...) не вызывает shell — не собирайте sh -c из пользовательского ввода.
  • Path traversal: os.Root (Go 1.24) ограничивает файловые операции каталогом; filepath.IsLocal.
  • http.Server без таймаутов → Slowloris; io.LimitReader/http.MaxBytesReader против огромных тел запросов.
  • Уязвимости зависимостей — govulncheck ./... (анализирует реально вызываемый код, а не просто список модулей).
  • Пароли — bcrypt/argon2id из golang.org/x/crypto.

28. Как отлаживать упавший или зависший процесс в проде?

  • GOTRACEBACK=all (стеки всех горутин при панике), =crash — ещё и core dump; debug.SetTraceback из кода.
  • Зависший процесс: kill -QUIT <pid> (SIGQUIT) — рантайм печатает стеки всех горутин и выходит. Без остановки — /debug/pprof/goroutine?debug=2.
  • Delve: dlv attach <pid>, dlv core ./app core.123, dlv debug; для оптимизированного кода — собирать с -gcflags=all="-N -l".
  • fatal error (concurrent map writes, deadlock, out of memory) не ловится recover — смотрите стек из лога.
  • GODEBUG=gctrace=1,schedtrace=1000,scheddetail=1 — живая телеметрия GC и планировщика в stderr.

Что нового в Go 1.22–1.27: шпаргалка к собеседованию 2026

Интервьюеры любят спросить «что появилось в последних версиях Go?». Этот ответ показывает, что вы следите за языком. Источник — официальные release notes.


Go 1.27 (август 2026)

  • Generic-методы: методы могут объявлять собственные параметры типа (func (s S[T]) Map[R any](...)). В интерфейсах — всё ещё нельзя.
  • В ключах составных литералов структур можно использовать селекторы вложенных полей.
  • Инференс типов для дженерик-функций во всех контекстах присваивания.
  • encoding/json/v2 и encoding/json/jsontext — новый быстрый и строгий JSON (отключение: GOEXPERIMENT=nojsonv2).
  • Новый пакет uuid (uuid.New(), uuid.Parse()).
  • crypto/mldsa — постквантовые подписи ML-DSA (FIPS 204), интеграция с TLS.
  • Профиль утечек горутин goroutineleak доступен по умолчанию.
  • Size-specialized malloc: мелкие аллокации до 30% быстрее.
  • strings.CutLast, bytes.CutLast, url.URL.Clone, httptest.NewTestServer, synctest.Sleep.
  • Экспериментальный портируемый пакет simd (GOEXPERIMENT=simd).
  • asynctimerchan удалён: каналы таймеров всегда синхронные. go test по умолчанию запускает vet-проверку stdversion.
  • Минимальная macOS — 13 Ventura.

Go 1.26 (февраль 2026)

  • new(expr): p := new(42), Age: new(calcAge(born)).
  • Самоссылающиеся ограничения дженериков: type Adder[A Adder[A]] interface{...}.
  • Green Tea GC по умолчанию (−10–40% накладных расходов GC).
  • cgo-вызовы ~30% быстрее; рандомизация базового адреса кучи.
  • errors.AsType[T], slog.NewMultiHandler, bytes.Buffer.Peek, итераторы в reflect (Type.Fields(), Type.Methods()).
  • io.ReadAll в 2 раза быстрее.
  • Переписанный go fix с модернайзерами и //go:fix inline.
  • Постквантовые гибридные обмены ключами в TLS включены по умолчанию; crypto/hpke.
  • Экспериментальные simd/archsimd, runtime/secret, профиль goroutineleak.

Go 1.25 (август 2025)

  • Container-aware GOMAXPROCS — учитывает CPU limit cgroup.
  • testing/synctest стабилен.
  • sync.WaitGroup.Go.
  • runtime/trace.FlightRecorder.
  • Экспериментальные Green Tea GC и encoding/json/v2.
  • Удалено понятие core types из спецификации; go vet анализаторы waitgroup и hostport.

Go 1.24 (февраль 2025)

  • Map на Swiss Tables, новая реализация sync.Map.
  • Параметризованные алиасы типов.
  • testing.B.Loop, t.Context(), t.Chdir().
  • runtime.AddCleanup, пакет weak, os.Root (доступ к файлам в пределах каталога).
  • omitzero в JSON, strings.Lines/SplitSeq/FieldsSeq.
  • Директива tool в go.mod, крипто: crypto/mlkem, crypto/hkdf, crypto/sha3, FIPS 140-3.

Go 1.23 (август 2024)

  • Итераторы (range-over-func), пакет iter, функции-итераторы в slices и maps.
  • Пакет unique (интернирование).
  • Таймеры: собираются GC без Stop, каналы таймеров синхронные.
  • Телеметрия тулчейна (opt-in).

Go 1.22 (февраль 2024)

  • Новая семантика переменной цикла — своя на каждой итерации.
  • for i := range 10.
  • Роутинг с методами и {wildcard} в http.ServeMux.
  • math/rand/v2.

System Design на собеседовании Go-разработчика (Middle+/Senior)


Алгоритм ответа (45–60 минут)

  1. Требования (5–10 мин). Функциональные: что делает система. Нефункциональные: RPS, объём данных, латентность (p99), доступность, консистентность. Задавайте вопросы — это оценивают.
  2. Оценка нагрузки (back-of-the-envelope). 10 млн DAU × 10 запросов / 86 400 с ≈ 1 200 RPS, пик ×3–5.
  3. API — эндпоинты или gRPC-методы.
  4. Модель данных и выбор хранилища.
  5. Высокоуровневая схема: клиент → LB → сервисы → кэш → БД → очередь.
  6. Углубление в узкое место, которое выберет интервьюер.
  7. Масштабирование и отказы: шардирование, репликация, ретраи, деградация.

Цифры, которые надо помнить

Операция Время
L1 cache ~1 нс
Mutex lock/unlock ~20 нс
Основная память ~100 нс
SSD random read ~16–100 мкс
Round trip в одном ДЦ ~0.5 мс
Чтение 1 МБ с SSD ~50–250 мкс
Round trip между континентами ~150 мс

Типовые задачи и ключевые идеи

Задача Ключевые решения
Сокращатель ссылок base62 от ID из генератора (Snowflake / диапазоны), кэш горячих ссылок, 301 vs 302 для аналитики, KV-хранилище
Rate limiter token bucket / sliding window в Redis + Lua, лимиты per-user/per-IP, где ставить (gateway) — код
Лента новостей fan-out on write (push) для обычных, fan-out on read (pull) для «звёзд», гибрид
Чат / мессенджер WebSocket, сервис присутствия, порядок сообщений в чате (sequence per chat), хранилище Cassandra/ScyllaDB, доставка офлайн через push
Уведомления очередь (Kafka), идемпотентность, ретраи с backoff, DLQ, rate limit на провайдера
Платёжная система идемпотентность, двойная запись (ledger), Saga, outbox, сверка (reconciliation), строгая консистентность
Маркетплейс: корзина и остатки резервирование с TTL, оптимистичные блокировки (version), избегать overselling
Сервис такси геоиндекс (geohash / H3 / S2), обновление координат водителей в памяти/Redis, матчинг
Счётчик просмотров батчинг (batcher), шардированные счётчики, HyperLogLog для уникальных, ClickHouse
Распределённый кэш consistent hashing, репликация, вытеснение (LRU), stampede (singleflight)

Концепции, которые должны отскакивать от зубов

  • CAP / PACELC, консистентность: strong, eventual, read-your-writes.
  • Репликация: leader-follower, multi-leader, leaderless (кворумы R + W > N).
  • Шардирование: по диапазону, по хэшу, consistent hashing; решардинг.
  • Консенсус: Raft (etcd, написан на Go), leader election.
  • Идемпотентность и доставка: at-most-once, at-least-once, exactly-once (на практике — at-least-once + дедупликация).
  • Кэширование: cache-aside, write-through, write-back; инвалидация; TTL с jitter.
  • Балансировка: L4 vs L7, round-robin, least connections, consistent hashing.
  • Observability: SLI/SLO/SLA, error budget, RED/USE.

Почему Go хорош для таких систем (стоит сказать)

Дешёвые горутины для IO-bound нагрузки, netpoller, предсказуемые паузы GC, статический бинарник для контейнеров. На Go написаны Kubernetes, Docker, etcd, Prometheus, CockroachDB, VictoriaMetrics, Consul, Terraform.


Распределённые системы и надёжность: вопросы Senior Go-разработчику

Senior-интервью в 2026 почти всегда включает блок «как ваш сервис ведёт себя, когда всё ломается». Ответ «добавим ретраи» без деталей — красный флаг.


1. Как правильно делать ретраи?

  • Повторять только идемпотентные операции и только временные ошибки (таймаут, 503, UNAVAILABLE), не 400/409.
  • Экспоненциальная задержка + jitter (full jitter: rand(0, min(cap, base·2ⁿ))), иначе клиенты синхронизируются и бьют сервер волнами.
  • Retry budget (например, ретраев не больше 10% от запросов) — иначе при деградации ретраи умножают нагрузку (retry storm): 3 уровня сервисов по 3 попытки = 27× нагрузки на нижний.
  • Ретраить на одном уровне стека, уважать дедлайн запроса (context), не спать дольше оставшегося времени.
  • Реализация — Retry с backoff и jitter.

2. Что такое Circuit Breaker и чем он отличается от ретраев?

Ретраи помогают пережить кратковременный сбой; circuit breaker защищает от длительного: после N ошибок подряд (или % ошибок в окне) переходит в Open и сразу отвечает ошибкой, не нагружая упавший сервис; через таймаут — Half-Open, пропускает пробный запрос; успех → Closed. Плюс fallback (кэш, дефолт, деградация фичи). Библиотеки: sony/gobreaker. Реализация — Circuit Breaker.

3. Как сделать API идемпотентным?

Клиент генерирует Idempotency-Key (UUID) на операцию и шлёт его при каждом ретрае. Сервер в транзакции: INSERT ... ON CONFLICT (key) DO NOTHING в таблицу ключей → если ключ уже есть, возвращает сохранённый ответ, а не выполняет операцию повторно. Хранить ключи с TTL. Для консьюмеров Kafka — дедупликация по ID сообщения (inbox pattern). Exactly-once в распределённой системе = at-least-once доставка + идемпотентная обработка.

4. Сага vs двухфазный коммит (2PC)

  • 2PC: координатор просит всех «подготовиться», затем «закоммитить». Строгая атомарность, но блокирующий: упал координатор между фазами — участники держат блокировки. Плохо масштабируется, не поддерживается большинством брокеров/HTTP-API.
  • Сага: цепочка локальных транзакций, для каждой — компенсирующее действие (отменить бронь, вернуть деньги). Нет изоляции (промежуточные состояния видны), нужна идемпотентность шагов и компенсаций.
    • Оркестрация — центральный координатор (Temporal, свой state machine в БД): проще отлаживать.
    • Хореография — сервисы реагируют на события друг друга: меньше связности, сложнее понять общий поток.
  • Надёжная публикация событий шага — через outbox.

5. Распределённая блокировка на Redis — что может пойти не так?

SET key token NX PX 30000 + снятие Lua-скриптом, сверяющим token. Проблемы: процесс уснул (GC-пауза, своп) дольше TTL → блокировка истекла, её взял другой, а первый «проснулся» и пишет. Redlock эту проблему не решает. Решение — fencing token: хранилище выдаёт монотонно растущий номер при захвате, а ресурс отклоняет запись с номером меньше уже виденного. Если нужна корректность, а не только эффективность, — etcd/ZooKeeper (lease + revision) или блокировка в самой БД (SELECT ... FOR UPDATE, advisory locks в Postgres).

6. Consistent hashing — зачем и как?

При hash(key) % N добавление узла перемещает почти все ключи. В consistent hashing узлы и ключи ставятся на кольцо; ключ принадлежит первому узлу по часовой стрелке → при изменении числа узлов перемещается ~1/N ключей. Виртуальные узлы (100–200 на физический) выравнивают нагрузку. Альтернативы: rendezvous hashing (HRW), jump consistent hash (без памяти, но только добавление в конец). Реализация — Consistent hashing.

7. Backpressure, load shedding, bulkhead

  • Backpressure — медленный потребитель замедляет производителя: ограниченные каналы/очереди, семафоры, HTTP 429 + Retry-After. Неограниченная очередь = отложенный OOM и огромная латентность.
  • Load shedding — при перегрузке сразу отказывать части запросов (по приоритету, по времени ожидания в очереди — запрос, который ждёт дольше своего дедлайна, бессмысленно обрабатывать).
  • Bulkhead — отдельные пулы (соединений, горутин) на разные зависимости, чтобы медленная зависимость не выела все ресурсы.
  • Adaptive concurrency limits (алгоритмы Vegas/Gradient, как в Netflix concurrency-limits) — лимит подстраивается по латентности.

8. Hedged requests — что это?

Приём из статьи Google «The Tail at Scale»: если ответ не пришёл за время ~p95, отправить дубликат запроса в другую реплику и взять первый ответ, отменив второй через context. Резко снижает p99 ценой ~5% дополнительной нагрузки. Только для идемпотентных чтений. Очень похоже на задачу «первый успешный ответ из N реплик».

9. Как распространять дедлайны и отмену между сервисами?

В gRPC дедлайн из context автоматически передаётся в заголовке grpc-timeout и восстанавливается на сервере — вся цепочка вызовов укладывается в исходный бюджет. В HTTP это нужно делать вручную (свой заголовок, например X-Request-Deadline) или через OpenTelemetry baggage. На сервере — сразу проверять остаток времени и не начинать работу, которая заведомо не успеет.

10. Как устроены leader election и шедулинг задач «ровно на одной реплике»?

Kubernetes: client-go/tools/leaderelection (Lease-объект). etcd: concurrency.NewElection на сессии с lease — при потере соединения lease истекает и лидерство переходит. Postgres: pg_try_advisory_lock. Всегда учитывайте, что бывший лидер может ещё не знать, что он больше не лидер (снова fencing), и что задача может выполниться дважды — делайте её идемпотентной.


Как проходит собеседование Go-разработчика в 2026 году


Типичные этапы

  1. Скрининг с рекрутером (15–30 мин) — опыт, ожидания, мотивация.
  2. Техническое интервью по Go (60–90 мин) — теория (этот репозиторий, разделы 01–10), «что выведет код» (tricky), небольшая задача на конкурентность.
  3. Алгоритмическая секция / live coding (45–60 мин) — 1–2 задачи уровня LeetCode Easy/Medium (раздел задач). Характерна для Яндекса, Google и крупных бигтехов.
  4. System design (60 мин) — для Middle+ и выше (раздел 14).
  5. Практическое/архитектурное задание или code review — найти баги в чужом коде (гонки, утечки, необработанные ошибки).
  6. Финальное интервью с командой / руководителем — поведенческие вопросы.

Что оценивают на live coding

  • Уточнение требований и граничных случаев до кода.
  • Рассуждение вслух: сначала наивное решение и его сложность, потом оптимизация.
  • Чистый, идиоматичный Go: обработка ошибок, имена, отсутствие гонок.
  • Самопроверка: прогон примеров, граничные случаи (пустой ввод, один элемент, дубликаты, переполнение).

Code review: частые «подложенные» баги

  • Гонка при записи в map / append из горутин.
  • Горутина без выхода (утечка), незакрытые resp.Body / rows.
  • defer в цикле.
  • Захват переменной цикла (для кода с go < 1.22 в go.mod!).
  • Типизированный nil в интерфейсе ошибки.
  • Копирование структуры с sync.Mutex.
  • http.Client без таймаута, context.Background() вместо входящего ctx.
  • SQL через fmt.Sprintf (инъекция).

Поведенческие вопросы (STAR: Situation, Task, Action, Result)

  • Самая сложная задача / инцидент в проде и как вы его разбирали.
  • Конфликт в команде или с продактом.
  • Техническое решение, о котором вы пожалели.
  • Как вы принимаете решения при неполной информации.

План подготовки

Срок Что делать
1–2 недели Разделы 01–06 + tricky. Каждый день 2 задачи из tasks
3–4 недели Разделы 07–13, все задачи на конкурентность без подсказок, с -race
5–6 недели System design (2–3 задачи в неделю вслух), mock-интервью с другом
Постоянно LeetCode: паттерны two pointers, sliding window, hash map, BFS/DFS, heap, binary search, DP

Полный чек-лист: CHECKLIST.md.


Практика

«Что выведет этот код?» — 25 каверзных задач по Go с собеседований

Формат, который есть почти на каждом Go-собеседовании в Яндексе, Ozon, Авито, Wildberries, Т-Банке и Сбере. Сначала ответьте сами, потом раскройте ответ. Все ответы проверены на Go 1.24+ с go 1.22+ в go.mod.


1. Слайс и append в общий массив

a := []int{1, 2, 3, 4}
b := a[:2]
b = append(b, 99)
fmt.Println(a)
Ответ

[1 2 99 4] — у b cap = 4, append пишет в общий базовый массив. Защита: a[:2:2].

2. Два append от одного слайса

s := make([]int, 0, 1)
s1 := append(s, 1)
s2 := append(s, 2)
fmt.Println(s1, s2)
Ответ

[2] [2] — оба append пишут в один и тот же элемент общего массива (места хватает, реаллокации нет).

3. Изменение слайса в функции

func add(s []int) { s = append(s, 4); s[0] = 100 }

s := make([]int, 3, 10)
add(s)
fmt.Println(s)
Ответ

[100 0 0] — s[0] изменился в общем массиве, но длина у вызывающего осталась 3.

4. append внутри range

s := []int{1, 2, 3}
for i := range s {
    s = append(s, i)
}
fmt.Println(s)
Ответ

[1 2 3 0 1 2] — выражение range вычисляется один раз, цикл не бесконечный.

5. Модификация значений в range

type User struct{ Age int }
users := []User{{1}, {2}}
for _, u := range users {
    u.Age++
}
fmt.Println(users)
Ответ

[{1} {2}] — u — копия. Правильно: for i := range users { users[i].Age++ }.

6. Аргументы defer

i := 1
defer fmt.Println(i)
i = 2
Ответ

1 — аргументы вычисляются в момент defer. С замыканием defer func(){ fmt.Println(i) }() было бы 2.

7. defer и именованный результат

func f() (n int) {
    defer func() { n *= 2 }()
    return 3
}
fmt.Println(f())
Ответ

6 — return 3 присваивает n = 3, затем выполняется defer.

8. Порядок defer

for i := range 3 {
    defer fmt.Print(i)
}
Ответ

210 — LIFO.

9. nil-интерфейс

type MyErr struct{}
func (*MyErr) Error() string { return "" }

func get() error {
    var e *MyErr
    return e
}
fmt.Println(get() == nil)
Ответ

false — интерфейс содержит тип *MyErr и значение nil. Интерфейс равен nil, только когда тип тоже nil.

10. То же с any

var p *int
var e any = p
fmt.Println(e == nil, p == nil)
Ответ

false true

11. Сравнение интерфейсов

var a, b any = []int{1}, []int{1}
fmt.Println(a == b)
Ответ

panic: runtime error: comparing uncomparable type []int. Компилируется, но падает в рантайме. А any(User{"a"}) == any(User{"a"}) → true.

12. Method value: value vs pointer receiver

type T struct{ name string }
func (t T) Val()  { fmt.Println(t.name) }
func (t *T) Ptr() { fmt.Println(t.name) }

t := T{"a"}
f1, f2 := t.Val, t.Ptr
t.name = "b"
f1(); f2()
Ответ

a затем b — t.Val копирует получатель в момент взятия метода, t.Ptr запоминает &t.

13. recover во вложенной функции

func helper() { fmt.Println(recover()) }

func main() {
    defer func() { helper() }()
    panic("boom")
}
Ответ

Печатает <nil>, программа падает с panic: boom. recover работает, только если вызван непосредственно отложенной функцией. defer helper() — сработал бы.

14. Паника в горутине

func main() {
    defer func() { recover() }()
    go func() { panic("boom") }()
    time.Sleep(time.Second)
}
Ответ

Программа падает. recover в main не ловит панику из другой горутины.

15. main не ждёт горутины

func main() {
    go fmt.Println("hello")
}
Ответ

Скорее всего ничего — main завершается, процесс умирает вместе со всеми горутинами.

16. Замыкание в цикле (Go 1.22+)

var wg sync.WaitGroup
for i := 0; i < 3; i++ {
    wg.Add(1)
    go func() { defer wg.Done(); fmt.Print(i) }()
}
wg.Wait()
Ответ

0, 1, 2 в произвольном порядке. До Go 1.22 (или с go 1.21 в go.mod!) — обычно 333.

17. wg.Add внутри горутины

var wg sync.WaitGroup
for i := range 3 {
    go func() { wg.Add(1); defer wg.Done(); fmt.Print(i) }()
}
wg.Wait()
Ответ

Недетерминированно: Wait может вернуться до того, как горутины вызвали Add → напечатано 0–3 числа. Add — до go, или wg.Go (Go 1.25).

18. Чтение из закрытого канала

ch := make(chan int, 2)
ch <- 1
close(ch)
v, ok := <-ch
fmt.Println(v, ok)
v, ok = <-ch
fmt.Println(v, ok)
Ответ

1 true, затем 0 false.

19. Запись в nil-канал

var ch chan int
ch <- 1
Ответ

fatal error: all goroutines are asleep - deadlock! — запись в nil-канал блокирует навсегда (единственная горутина).

20. Переполнение

var x uint8 = 255
x++
fmt.Println(x, -7/2, -7%2)
Ответ

0 -3 -1 — беззнаковое переполнение по модулю; деление усекается к нулю, знак остатка = знак делимого.

21. Длина строки и range

s := "héllo"
for i, r := range s { fmt.Print(i, string(r), " ") }
fmt.Println(len(s))
Ответ

0h 1é 3l 4l 5o 6 — é занимает 2 байта, range отдаёт байтовые индексы начала рун.

22. Массив — значение

a := [3]int{1, 2, 3}
b := a
b[0] = 100
fmt.Println(a, b)
Ответ

[1 2 3] [100 2 3]

23. copy

dst := make([]int, 2)
n := copy(dst, []int{7, 8, 9})
fmt.Println(n, dst)
Ответ

2 [7 8] — копируется min(len(dst), len(src)).

24. JSON и неэкспортируемые поля

type U struct {
    Name string
    age  int
}
b, _ := json.Marshal(U{"go", 5})
fmt.Println(string(b))
Ответ

{"Name":"go"} — рефлексия не видит неэкспортируемые поля.

25. Map структур

m := map[string]User{"a": {Age: 1}}
m["a"].Age = 2
Ответ

Ошибка компиляции: cannot assign to struct field m["a"].Age in map. Элементы map неадресуемы.


Хотите больше? Предложите свою задачу через issue или Pull Request.


«Что выведет этот код?» — Senior-уровень: ещё 15 задач

Задачи, на которых «сыпятся» даже опытные разработчики: NaN-ключи, итераторы, таймеры Go 1.23, дженерики и comparable, выравнивание. Все ответы проверены на Go 1.24 (go 1.24 в go.mod).


26. NaN как ключ map

m := map[float64]int{}
m[math.NaN()] = 1
m[math.NaN()] = 2
_, ok := m[math.NaN()]
fmt.Println(len(m), ok)
clear(m)
fmt.Println(len(m))
Ответ

2 false, затем 0. NaN != NaN, поэтому каждая запись создаёт новый ключ, а найти его невозможно. delete такие ключи тоже не удалит — только clear(m) (одна из причин, почему clear появился в Go 1.21).

27. len и range по nil-указателю на массив

var p *[5]int
fmt.Println(len(p))
for i := range p {
	fmt.Print(i)
}
Ответ

5 и 01234 — без паники. Длина массива — часть типа, это константа; range только по индексам не разыменовывает указатель. А вот for i, v := range p — уже panic: nil pointer dereference.

28. Method value и defer

type T struct{ n int }
func (t T) Print() { fmt.Println("T", t.n) }

t := T{1}
defer t.Print()
f := t.Print
t.n = 2
f()
Ответ

T 1 и T 1. И defer t.Print(), и f := t.Print копируют получатель (value receiver) в момент вычисления. С pointer receiver оба вывели бы T 2.

29. Дженерики и comparable

func eq[V comparable](a, b V) bool { return a == b }

fmt.Println(eq[any](1, 1))
fmt.Println(eq[any]([]int{}, []int{}))
Ответ

true, затем panic: runtime error: comparing uncomparable type []int. С Go 1.20 any удовлетворяет comparable («strictly comparable» не требуется), поэтому код компилируется, но сравнение интерфейсов с несравнимыми динамическими типами паникует в рантайме.

30. Таймер после Go 1.23

t := time.NewTimer(10 * time.Millisecond)
time.Sleep(20 * time.Millisecond)
t.Reset(time.Hour)
select {
case <-t.C:
	fmt.Println("stale fire")
default:
	fmt.Println("no stale value")
}
Ответ

С go 1.23+ в go.mod: no stale value — канал таймера синхронный, Reset гарантирует, что старое срабатывание не будет прочитано. С go 1.22 и ниже в go.mod (или GODEBUG=asynctimerchan=1): stale fire — значение уже лежало в буфере канала.

31. min/max с NaN и нулями

fmt.Println(min(1.0, math.NaN(), -1.0), max(0.0, -0.0))
Ответ

NaN 0. Встроенные min/max (Go 1.21) возвращают NaN, если любой аргумент NaN. Вторая ловушка: константа -0.0 в Go — это обычный 0 (константы точные), отрицательного нуля тут вообще нет. Настоящий -0 получают через math.Copysign(0, -1), и для него min(0.0, nz) = -0, max(nz, 0.0) = 0 — встроенные функции считают -0 < +0.

32. Размер структуры

type A struct {
	a bool
	b int64
	c bool
}
type B struct {
	b int64
	a bool
	c bool
}
fmt.Println(unsafe.Sizeof(A{}), unsafe.Sizeof(B{}))
Ответ

24 16 (amd64). В A после a 7 байт padding до выравнивания int64, после c — ещё 7 до кратности 8. Правило: сортировать поля по убыванию размера. Линтер — fieldalignment.

33. select с закрытым и nil-каналом

ch := make(chan int)
close(ch)
var nilCh chan int
cnt := 0
for i := 0; i < 5; i++ {
	select {
	case <-ch:
		cnt++
	case <-nilCh:
	}
}
fmt.Println(cnt)
Ответ

5. Закрытый канал всегда готов на чтение (zero value), nil-канал — никогда. Поэтому в fan-in закрытый канал «выключают», присваивая переменной nil.

34. defer в цикле по range int

for i := range 3 {
	defer func() { fmt.Print(i) }()
}
Ответ

210. С Go 1.22 у каждой итерации своя i (замыкания видят 0, 1, 2), а defer выполняются в обратном порядке. До 1.22 было бы 333.

35. Полное выражение среза и append

s := []int{1, 2, 3, 4}
s2 := s[:2:2]
s2 = append(s2, 9)
fmt.Println(s, s2)
Ответ

[1 2 3 4] [1 2 9]. cap(s2) == 2, append вынужден выделить новый массив, исходный не затронут.

36. Перекрывающийся copy

s := []int{1, 2, 3, 4, 5}
copy(s[1:], s)
fmt.Println(s)
Ответ

[1 1 2 3 4]. copy корректно работает с перекрывающимися областями (как memmove), поэтому это стандартный способ вставить элемент со сдвигом.

37. Итератор и break

func gen() iter.Seq[int] {
	return func(yield func(int) bool) {
		defer fmt.Println("cleanup")
		for i := 0; ; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

for v := range gen() {
	if v == 2 {
		break
	}
}
Ответ

cleanup. break превращается в return false из yield, итератор завершается, его defer выполняется. Бесконечный генератор безопасен, если уважает результат yield.

38. Итератор, который не проверяет yield

func bad(yield func(int) bool) {
	for i := 0; i < 3; i++ {
		yield(i)
	}
}

for v := range bad {
	if v == 1 {
		break
	}
}
Ответ

panic: runtime error: range function continued iteration after function for loop body returned false. Итератор обязан остановиться, когда yield вернул false.

39. Паника внутри sync.Once

var once sync.Once
func() {
	defer func() { recover() }()
	once.Do(func() { panic("boom") })
}()
called := false
once.Do(func() { called = true })
fmt.Println(called)
Ответ

false. Once считает функцию выполненной, даже если она запаниковала, — повторного вызова не будет, а инициализация осталась незавершённой. sync.OnceValue/OnceFunc (1.21) в такой ситуации повторяют ту же панику при каждом вызове — это безопаснее.

40. errors.Is со значением-структурой

type E struct{ msg string }
func (e E) Error() string { return e.msg }

err := fmt.Errorf("wrap: %w", E{"x"})
fmt.Println(errors.Is(err, E{"x"}), errors.Is(err, E{"y"}))
Ответ

true false. errors.Is сравнивает через ==, а структура из сравнимых полей сравнивается по значению. Если бы в E было поле-слайс, == вызвал бы панику — поэтому errors.Is сначала проверяет сравнимость типа (reflectlite) и такие ошибки просто не совпадут.


Задачи с Go-собеседований: решения, разборы и тесты

Каждая задача — отдельный Go-пакет: условие, разбор, ловушки, follow-up вопросы и решение на Go. Все решения проверены go test -race.

Как тренироваться: прочитайте условие, закройте решение, напишите своё за 20–30 минут, сравните с решением.

Алгоритмы (live coding)

# Задача Паттерн Уровень
1 Two Sum hash map, два указателя Junior
2 Правильная скобочная последовательность стек Junior
3 Пересечение двух слайсов hash map, generics Junior
4 RLE-сжатие строки строки, strings.Builder Junior
5 Бинарный поиск и сдвинутый массив binary search Junior+
6 Связный список: разворот, цикл, слияние указатели, slow/fast Junior+
7 Группировка анаграмм hash map, массив как ключ Junior+
8 Самая длинная подстрока без повторов sliding window, руны Middle
9 Слияние интервалов сортировка Middle
10 Top K частых элементов heap, bucket sort Middle
11 LRU-кэш map + двусвязный список, generics Middle/Senior

Конкурентность (главное на Go-собесе)

# Задача Что проверяют Уровень
1 Worker pool WaitGroup, закрытие каналов, отмена Middle
2 Fan-in: слить N каналов WaitGroup, nil-каналы в select Middle
3 Pipeline владение каналами, отмена Middle
4 Первый ответ из N реплик утечки горутин, errors.Join Middle
5 Кэш с TTL RWMutex, janitor, sync.Once Middle
6 Graceful shutdown HTTP сигналы, Shutdown, Kubernetes Middle
7 Параллельно с лимитом и отменой (errgroup) семафор, WithCancelCause Middle/Senior
8 Rate limiter (token bucket) время, мьютекс, тестируемость Middle/Senior
9 Батчер: по размеру или таймауту таймеры, select Middle/Senior
10 Pub/Sub брокер RWMutex, закрытие каналов Middle/Senior
11 Singleflight cache stampede, data race Senior

Продвинутые задачи (Senior+)

# Задача Что проверяют Уровень
1 Circuit Breaker конечный автомат, half-open, тестируемость Senior
2 Consistent hashing виртуальные узлы, бинарный поиск Senior
3 Шардированная map шардирование блокировок, maphash, false sharing Middle/Senior
4 Lock-free стек CAS, atomic.Pointer, ABA Senior
5 Retry с backoff и jitter context, классификация ошибок Middle/Senior

Two Sum — найти два числа с заданной суммой

Уровень: Junior+ · Где спрашивают: Яндекс, Ozon, Авито, Google (разминка)

Условие

Дан массив nums и число target. Верните индексы двух разных элементов, сумма которых равна target.

Разбор

  1. Наивно — два вложенных цикла, O(n²). Назовите это вслух и сразу предложите лучше.
  2. Hash map — идём слева направо, для v ищем target-v среди уже увиденных. O(n) время / O(n) память.
  3. Массив отсортирован — два указателя, O(n) / O(1).

Частые ошибки

  • Кладут элемент в map до проверки → находят пару (i, i) при target = 2*v.
  • Забывают про пустой массив и отсутствие ответа.

Follow-up на собесе

  • 3Sum / kSum? → сортировка + два указателя, O(n²).
  • Данные не влезают в память? → внешняя сортировка / шардирование по v mod k.

Решение на Go (twosum.go):

// Package twosum — классика: найти индексы двух чисел, дающих в сумме target.
package twosum

// TwoSum возвращает индексы i<j, где nums[i]+nums[j]==target.
// Время O(n), память O(n): один проход с map «значение → индекс».
func TwoSum(nums []int, target int) (int, int, bool) {
	seen := make(map[int]int, len(nums))
	for j, v := range nums {
		if i, ok := seen[target-v]; ok {
			return i, j, true
		}
		seen[v] = j
	}
	return -1, -1, false
}

// TwoSumSorted — вариант для отсортированного массива: два указателя, O(n) время, O(1) память.
func TwoSumSorted(nums []int, target int) (int, int, bool) {
	l, r := 0, len(nums)-1
	for l < r {
		switch s := nums[l] + nums[r]; {
		case s == target:
			return l, r, true
		case s < target:
			l++
		default:
			r--
		}
	}
	return -1, -1, false
}

Правильная скобочная последовательность

Уровень: Junior · Где спрашивают: почти везде как разминка

Условие

Строка содержит ()[]{}. Проверить, что каждая открывающая скобка закрыта скобкой того же типа в правильном порядке.

Разбор

Стек: открывающую кладём, на закрывающей сверяем с вершиной. В конце стек должен быть пуст. Сложность O(n) / O(n). Если скобка одного типа — хватит счётчика, O(1) памяти.

Частые ошибки

  • Не проверяют пустой стек перед stack[len(stack)-1] → panic: index out of range.
  • Забывают проверить len(stack) == 0 в конце ("((" вернёт true).

Решение на Go (brackets.go):

// Package validparentheses — проверка правильной скобочной последовательности.
package validparentheses

var pairs = map[rune]rune{')': '(', ']': '[', '}': '{'}

// IsValid проверяет строку из скобок ()[]{} (прочие символы игнорируются). O(n) / O(n).
func IsValid(s string) bool {
	stack := make([]rune, 0, len(s))
	for _, r := range s {
		switch r {
		case '(', '[', '{':
			stack = append(stack, r)
		case ')', ']', '}':
			if len(stack) == 0 || stack[len(stack)-1] != pairs[r] {
				return false
			}
			stack = stack[:len(stack)-1]
		}
	}
	return len(stack) == 0
}

Пересечение двух слайсов

Уровень: Junior · Где спрашивают: Wildberries, Ozon, Авито, МТС, Сбер — первая задача live-coding

Условие

Даны два неотсортированных слайса. Верните их пересечение.

Уточните у интервьюера (это оценивают!)

  1. Учитывать кратность? ([2,2] ∩ [2] = [2] или [2,2]?)
  2. Важен ли порядок?
  3. Какие размеры? Помещается ли в память?

Решения

  • map[T]int счётчиков — O(n+m).
  • Оба отсортированы — два указателя, O(n+m) / O(1).
  • Один сильно меньше другого — map по меньшему.

Go-моменты

  • map[T]struct{} для множества — значение не занимает память.
  • Дженерик [T comparable] — ключ map обязан быть comparable.

Решение на Go (intersect.go):

// Package intersect — пересечение двух слайсов (топ-вопрос на русскоязычных Go-собесах).
package intersect

// Intersect возвращает элементы, встречающиеся в обоих слайсах, с учётом кратности,
// в порядке их появления во втором слайсе. O(n+m).
func Intersect[T comparable](a, b []T) []T {
	cnt := make(map[T]int, len(a))
	for _, v := range a {
		cnt[v]++
	}
	var out []T
	for _, v := range b {
		if cnt[v] > 0 {
			cnt[v]--
			out = append(out, v)
		}
	}
	return out
}

// Unique — пересечение как множеств (без повторов).
func Unique[T comparable](a, b []T) []T {
	set := make(map[T]struct{}, len(a)) // struct{} занимает 0 байт
	for _, v := range a {
		set[v] = struct{}{}
	}
	var out []T
	for _, v := range b {
		if _, ok := set[v]; ok {
			out = append(out, v)
			delete(set, v)
		}
	}
	return out
}

RLE-сжатие строки

Уровень: Junior · Где спрашивают: Яндекс (разминка), банки

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

  • Аккуратность с границами (последняя группа!).
  • strings.Builder вместо конкатенации += (которая даёт O(n²) аллокаций).
  • Unicode: работа с []rune, а не с байтами.
  • Счётчик ≥ 10 ("A11") — многие пишут byte('0'+n) и ломаются.

Решение на Go (rle.go):

// Package rle — сжатие строки Run-Length Encoding: "AAABBC" → "A3B2C".
package rle

import (
	"strconv"
	"strings"
)

// Encode кодирует строку (Unicode-safe). Одиночный символ пишется без счётчика.
func Encode(s string) string {
	r := []rune(s)
	var b strings.Builder
	b.Grow(len(s))
	for i := 0; i < len(r); {
		j := i
		for j < len(r) && r[j] == r[i] {
			j++
		}
		b.WriteRune(r[i])
		if n := j - i; n > 1 {
			b.WriteString(strconv.Itoa(n))
		}
		i = j
	}
	return b.String()
}

Бинарный поиск: lower bound и сдвинутый массив

Уровень: Junior+/Middle

Почему это спрашивают

Бинпоиск — 5 строк, в которых легко ошибиться на ±1. Интервьюер смотрит, умеете ли вы держать инвариант.

Шпаргалка

  • Полуинтервал [lo, hi): for lo < hi, hi = mid, lo = mid + 1.
  • mid := int(uint(lo+hi) >> 1) или lo + (hi-lo)/2 — защита от переполнения.
  • В стандартной библиотеке: slices.BinarySearch, slices.BinarySearchFunc, sort.Search.

Варианты задачи

  • Первое/последнее вхождение, sqrt(x), поиск в сдвинутом массиве, «бинпоиск по ответу» (минимальная скорость/вместимость).

Решение на Go (bs.go):

// Package binarysearch — бинарный поиск и lower bound без багов на границах.
package binarysearch

import "cmp"

// LowerBound возвращает первый индекс i, где xs[i] >= target (или len(xs)). Инвариант: [lo, hi).
func LowerBound[T cmp.Ordered](xs []T, target T) int {
	lo, hi := 0, len(xs)
	for lo < hi {
		mid := int(uint(lo+hi) >> 1) // без переполнения (так сделано в sort.Search)
		if xs[mid] < target {
			lo = mid + 1
		} else {
			hi = mid
		}
	}
	return lo
}

// Search возвращает индекс target или -1.
func Search[T cmp.Ordered](xs []T, target T) int {
	if i := LowerBound(xs, target); i < len(xs) && xs[i] == target {
		return i
	}
	return -1
}

// SearchRotated — поиск в отсортированном и циклически сдвинутом массиве без дубликатов. O(log n).
func SearchRotated(xs []int, target int) int {
	lo, hi := 0, len(xs)-1
	for lo <= hi {
		mid := lo + (hi-lo)/2
		if xs[mid] == target {
			return mid
		}
		if xs[lo] <= xs[mid] { // левая половина отсортирована
			if xs[lo] <= target && target < xs[mid] {
				hi = mid - 1
			} else {
				lo = mid + 1
			}
		} else { // правая половина отсортирована
			if xs[mid] < target && target <= xs[hi] {
				lo = mid + 1
			} else {
				hi = mid - 1
			}
		}
	}
	return -1
}

Связный список: разворот, цикл, середина, слияние

Уровень: Junior+/Middle

Задача Приём Сложность
Развернуть список три указателя prev/cur/next O(n) / O(1)
Есть ли цикл Флойд: slow +1, fast +2 O(n) / O(1)
Середина тот же slow/fast O(n) / O(1)
Слить два отсортированных dummy-голова O(n+m) / O(1)

Go-фишка: параллельное присваивание head.Next, prev, head = prev, head, head.Next — все правые части вычисляются до присваивания. Будьте готовы объяснить, почему это корректно.

Решение на Go (list.go):

// Package linkedlist — разворот списка, поиск цикла, середина, слияние.
package linkedlist

// Node — узел односвязного списка.
type Node struct {
	Val  int
	Next *Node
}

// FromSlice строит список (удобно для тестов).
func FromSlice(xs []int) *Node {
	var head *Node
	for i := len(xs) - 1; i >= 0; i-- {
		head = &Node{xs[i], head}
	}
	return head
}

// ToSlice — обратное преобразование.
func (n *Node) ToSlice() []int {
	var out []int
	for ; n != nil; n = n.Next {
		out = append(out, n.Val)
	}
	return out
}

// Reverse разворачивает список итеративно. O(n) / O(1).
func Reverse(head *Node) *Node {
	var prev *Node
	for head != nil {
		head.Next, prev, head = prev, head, head.Next
	}
	return prev
}

// HasCycle — алгоритм Флойда «черепаха и заяц». O(n) / O(1).
func HasCycle(head *Node) bool {
	slow, fast := head, head
	for fast != nil && fast.Next != nil {
		slow, fast = slow.Next, fast.Next.Next
		if slow == fast {
			return true
		}
	}
	return false
}

// Middle возвращает средний узел (для чётной длины — второй из двух средних).
func Middle(head *Node) *Node {
	slow, fast := head, head
	for fast != nil && fast.Next != nil {
		slow, fast = slow.Next, fast.Next.Next
	}
	return slow
}

// MergeSorted сливает два отсортированных списка. Приём: фиктивная голова (dummy).
func MergeSorted(a, b *Node) *Node {
	dummy := &Node{}
	tail := dummy
	for a != nil && b != nil {
		if a.Val <= b.Val {
			tail.Next, a = a, a.Next
		} else {
			tail.Next, b = b, b.Next
		}
		tail = tail.Next
	}
	if a != nil {
		tail.Next = a
	} else {
		tail.Next = b
	}
	return dummy.Next
}

Group Anagrams — группировка анаграмм

Уровень: Junior+/Middle

Идея

Нужен канонический ключ анаграммы:

  1. Отсортированные руны слова — O(L log L), работает с Unicode.
  2. Массив счётчиков [26]byte — O(L). Фишка Go: массивы comparable, их можно использовать как ключ map (слайсы — нельзя).

Вопросы вдогонку

  • Почему []byte нельзя ключом map? — слайсы не comparable (нет ==).
  • Что будет с "ё" в варианте с [26]byte? — выход за границы / неверный индекс → нужен map[rune]int или вариант с сортировкой.
  • string(r) для []rune — аллокация. Оптимизация: strings.Builder или хэш.

Решение на Go (anagrams.go):

// Package groupanagrams — группировка анаграмм.
package groupanagrams

import (
	"slices"
)

// Group группирует слова-анаграммы (строчные латинские буквы).
// Ключ — массив счётчиков [26]byte: массивы в Go comparable и годятся как ключ map. O(n·L).
func Group(words []string) [][]string {
	groups := make(map[[26]byte][]string)
	order := make([][26]byte, 0) // сохраняем порядок первого появления — детерминированный вывод
	for _, w := range words {
		var key [26]byte
		for i := 0; i < len(w); i++ {
			key[w[i]-'a']++
		}
		if _, ok := groups[key]; !ok {
			order = append(order, key)
		}
		groups[key] = append(groups[key], w)
	}
	out := make([][]string, 0, len(order))
	for _, k := range order {
		out = append(out, groups[k])
	}
	return out
}

// GroupSorted — альтернатива с ключом «отсортированное слово», O(n·L log L), работает для любого Unicode.
func GroupSorted(words []string) map[string][]string {
	groups := make(map[string][]string)
	for _, w := range words {
		r := []rune(w)
		slices.Sort(r)
		groups[string(r)] = append(groups[string(r)], w)
	}
	return groups
}

Longest Substring Without Repeating Characters

Уровень: Middle · Паттерн: скользящее окно (sliding window)

Разбор

Держим окно [left, i] без повторов. Встретили символ, который уже есть внутри окна — сдвигаем left за его прошлую позицию. O(n).

Ловушки

  • "abba": при втором a его прошлая позиция 0 < left=2 — сдвигать left назад нельзя. Отсюда условие p >= left.
  • Байты vs руны. s[i] — байт; for _, r := range s — руны (UTF-8). len("привет") == 12. Интервьюер почти наверняка спросит, что будет с кириллицей.

Решение на Go (window.go):

// Package longestsubstring — самая длинная подстрока без повторяющихся символов (sliding window).
package longestsubstring

// Longest возвращает длину (в рунах) самой длинной подстроки без повторов. O(n) / O(alphabet).
func Longest(s string) int {
	last := make(map[rune]int) // руна → индекс (в рунах) последнего появления
	best, left, i := 0, 0, 0
	for _, r := range s { // range по строке идёт по рунам, а не по байтам
		if p, ok := last[r]; ok && p >= left {
			left = p + 1
		}
		last[r] = i
		best = max(best, i-left+1)
		i++
	}
	return best
}

Merge Intervals — слияние отрезков

Уровень: Middle · Где спрашивают: Яндекс, Т-Банк, Google, Avito

Условие

Дан список отрезков [start, end]. Слейте все пересекающиеся.

Разбор

Сортируем по start, идём по списку и расширяем последний отрезок результата, пока новый начинается не позже его конца. O(n log n) время, O(n) память.

Go-нюансы, которые любят спрашивать

  • slices.SortFunc + cmp.Compare (Go 1.21+) вместо sort.Slice — типобезопасно и быстрее.
  • last := &out[len(out)-1] — указатель на элемент слайса. Безопасно, пока не было append в этой итерации: после реаллокации указатель смотрел бы в старый массив.
  • Встроенные min/max (Go 1.21+).
  • slices.Clone — не мутируем вход (важно показать интервьюеру, что вы об этом подумали).

Решение на Go (merge.go):

// Package mergeintervals — слияние пересекающихся отрезков.
package mergeintervals

import (
	"cmp"
	"slices"
)

// Interval — отрезок [Start, End] включительно.
type Interval struct{ Start, End int }

// Merge сливает пересекающиеся и касающиеся отрезки. O(n log n) из-за сортировки.
// Входной слайс не модифицируется.
func Merge(in []Interval) []Interval {
	if len(in) == 0 {
		return nil
	}
	xs := slices.Clone(in)
	slices.SortFunc(xs, func(a, b Interval) int { return cmp.Compare(a.Start, b.Start) })

	out := []Interval{xs[0]}
	for _, cur := range xs[1:] {
		last := &out[len(out)-1]
		if cur.Start <= last.End {
			last.End = max(last.End, cur.End)
			continue
		}
		out = append(out, cur)
	}
	return out
}

Top K Frequent — K самых частых элементов

Уровень: Middle · Где спрашивают: Яндекс, Google, Avito

Разбор трёх решений

Подход Время Память Когда
Посчитать + отсортировать всё O(n log n) O(n) Быстро написать
Min-heap размера k (container/heap) O(n log k) O(n) k ≪ n, потоковые данные
Bucket sort по частоте O(n) O(n) Частоты ограничены n

Go-нюансы

  • container/heap требует реализовать heap.Interface (5 методов). Push/Pop — с указательным ресивером.
  • Порядок обхода map случайный → без tie-break результат недетерминирован, тесты «мигают».

Follow-up

  • Поток бесконечный, памяти мало → Count-Min Sketch + heap (приближённо), Misra–Gries.
  • Распределённо → map-reduce: локальные топы + слияние (точно только для top-k с запасом).

Решение на Go (topk.go):

// Package topk — K самых частых элементов.
package topk

import (
	"cmp"
	"container/heap"
	"slices"
)

type pair struct {
	val  string
	freq int
}

// minHeap по частоте: на вершине — наименее частый из текущего топа.
type minHeap []pair

func (h minHeap) Len() int { return len(h) }
func (h minHeap) Less(i, j int) bool {
	if h[i].freq != h[j].freq {
		return h[i].freq < h[j].freq
	}
	return h[i].val > h[j].val // детерминизм: при равенстве выкидываем лексикографически больший
}
func (h minHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *minHeap) Push(x any)   { *h = append(*h, x.(pair)) }
func (h *minHeap) Pop() any {
	old := *h
	x := old[len(old)-1]
	*h = old[:len(old)-1]
	return x
}

// TopKFrequent — O(n log k) через min-heap размера k.
// Результат упорядочен по убыванию частоты, при равенстве — по возрастанию строки.
func TopKFrequent(words []string, k int) []string {
	freq := make(map[string]int)
	for _, w := range words {
		freq[w]++
	}
	h := &minHeap{}
	for v, f := range freq {
		heap.Push(h, pair{v, f})
		if h.Len() > k {
			heap.Pop(h)
		}
	}
	out := make([]pair, h.Len())
	copy(out, *h)
	slices.SortFunc(out, func(a, b pair) int {
		if c := cmp.Compare(b.freq, a.freq); c != 0 {
			return c
		}
		return cmp.Compare(a.val, b.val)
	})
	res := make([]string, len(out))
	for i, p := range out {
		res[i] = p.val
	}
	return res
}

// TopKBucket — O(n) через bucket sort по частоте (частота ≤ n).
func TopKBucket(words []string, k int) []string {
	freq := make(map[string]int)
	for _, w := range words {
		freq[w]++
	}
	buckets := make([][]string, len(words)+1)
	for v, f := range freq {
		buckets[f] = append(buckets[f], v)
	}
	res := make([]string, 0, k)
	for f := len(buckets) - 1; f > 0 && len(res) < k; f-- {
		slices.Sort(buckets[f])
		for _, v := range buckets[f] {
			if len(res) == k {
				break
			}
			res = append(res, v)
		}
	}
	return res
}

LRU Cache на Go (generics + container/list)

Уровень: Middle/Senior · Где спрашивают: Яндекс, Ozon, VK, Wildberries, Google

Условие

Реализовать кэш фиксированного размера с операциями Get(key) и Put(key, value) за O(1). При переполнении вытесняется давно неиспользуемый ключ (Least Recently Used).

Разбор

  • map[K]*list.Element — поиск за O(1).
  • Двусвязный список (container/list) — порядок использования: голова = свежий, хвост = кандидат на вытеснение.
  • В элементе списка храним и ключ, иначе при вытеснении хвоста не сможем удалить его из map.

Что спросят дальше

Вопрос Ответ
Потокобезопасность? sync.Mutex. RWMutex не поможет: Get тоже меняет порядок.
Как снизить contention? Шардирование: N независимых кэшей, шард = hash(key) % N.
TTL? Храним expiresAt, проверяем в Get + фоновая очистка. См. ttlcache.
Почему не sync.Map? Нет порядка вытеснения; оптимизирован под read-mostly / disjoint keys.
LFU? map ключ→узел + map частота→список, minFreq.

Решение на Go (lru.go):

// Package lrucache — LRU-кэш с O(1) Get/Put. Самая частая «дизайн-задача» на Go-собесе.
package lrucache

import (
	"container/list"
	"sync"
)

type entry[K comparable, V any] struct {
	key K
	val V
}

// Cache — потокобезопасный LRU-кэш фиксированной ёмкости.
// map даёт O(1) поиск, двусвязный список — O(1) перенос в начало и удаление хвоста.
type Cache[K comparable, V any] struct {
	mu    sync.Mutex
	cap   int
	ll    *list.List
	items map[K]*list.Element
}

// New создаёт кэш. capacity <= 0 → panic (некорректная конфигурация — ошибка программиста).
func New[K comparable, V any](capacity int) *Cache[K, V] {
	if capacity <= 0 {
		panic("lrucache: capacity must be > 0")
	}
	return &Cache[K, V]{cap: capacity, ll: list.New(), items: make(map[K]*list.Element, capacity)}
}

// Get возвращает значение и помечает ключ как недавно использованный.
func (c *Cache[K, V]) Get(key K) (V, bool) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		c.ll.MoveToFront(el)
		return el.Value.(*entry[K, V]).val, true
	}
	var zero V
	return zero, false
}

// Put добавляет/обновляет значение; при переполнении вытесняет самый старый ключ.
func (c *Cache[K, V]) Put(key K, val V) {
	c.mu.Lock()
	defer c.mu.Unlock()
	if el, ok := c.items[key]; ok {
		el.Value.(*entry[K, V]).val = val
		c.ll.MoveToFront(el)
		return
	}
	c.items[key] = c.ll.PushFront(&entry[K, V]{key, val})
	if c.ll.Len() > c.cap {
		oldest := c.ll.Back()
		c.ll.Remove(oldest)
		delete(c.items, oldest.Value.(*entry[K, V]).key)
	}
}

// Len — текущее число элементов.
func (c *Cache[K, V]) Len() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.ll.Len()
}

Worker Pool на Go

Уровень: Middle · Где спрашивают: везде. Задача №1 по конкурентности на Go-собесе.

Условие

Обработать поток задач, запуская не более N обработчиков одновременно. Поддержать отмену через context.

Ключевые моменты, которые проверяют

  1. Кто закрывает канал результатов? Только отправитель и только после завершения всех отправителей → отдельная горутина wg.Wait(); close(out).
  2. Утечки горутин. Отправка в out должна быть в select с ctx.Done(), иначе при уходе читателя воркер повиснет навсегда.
  3. wg.Add до запуска горутины, не внутри неё (иначе Wait может проскочить).
  4. С Go 1.22 переменная цикла своя на каждой итерации — старый трюк i := i не нужен.
  5. С Go 1.25 есть wg.Go(func(){...}) — сам делает Add(1)/Done().

Альтернативы

  • Семафор на буферизированном канале: sem := make(chan struct{}, N).
  • golang.org/x/sync/errgroup с g.SetLimit(N) — стандарт де-факто в проде. См. parallel.

Решение на Go (pool.go):

// Package workerpool — пул воркеров с ограничением параллелизма, отменой и сбором ошибок.
package workerpool

import (
	"context"
	"sync"
)

// Result — результат обработки одной задачи.
type Result[T, R any] struct {
	Job T
	Val R
	Err error
}

// Run запускает workers горутин, читающих из jobs, и возвращает канал результатов.
// Канал результатов закрывается, когда все воркеры завершились
// (jobs закрыт или ctx отменён). Порядок результатов не гарантирован.
func Run[T, R any](ctx context.Context, workers int, jobs <-chan T, fn func(context.Context, T) (R, error)) <-chan Result[T, R] {
	if workers < 1 {
		workers = 1
	}
	out := make(chan Result[T, R])
	var wg sync.WaitGroup
	wg.Add(workers)
	for range workers { // range по int — Go 1.22+
		go func() {
			defer wg.Done()
			for {
				select {
				case <-ctx.Done():
					return
				case job, ok := <-jobs:
					if !ok {
						return
					}
					v, err := fn(ctx, job)
					select { // отправка тоже должна уважать отмену, иначе утечка горутины
					case out <- Result[T, R]{job, v, err}:
					case <-ctx.Done():
						return
					}
				}
			}
		}()
	}
	go func() {
		wg.Wait()
		close(out) // закрывает тот, кто пишет, — и только после всех писателей
	}()
	return out
}

Fan-in: слить N каналов в один

Уровень: Middle · Классика русскоязычных собесов: «напишите функцию merge(chans ...<-chan int) <-chan int»

Решение

По горутине на входной канал + WaitGroup + закрытие выхода после wg.Wait().

Ловушки

  • close(out) внутри горутины-читателя → panic: close of closed channel / send on closed channel.
  • Забыли ctx → если потребитель перестал читать, все горутины висят (goroutine leak). С Go 1.26+ такие утечки ловит профиль goroutineleak (включён по умолчанию с Go 1.27).
  • Трюк с nil-каналом: чтение из nil-канала блокирует навсегда → закрытый канал в select «отключают» присваиванием nil.

Follow-up

  • Сохранить порядок? → слияние отсортированных потоков через heap.
  • Fan-out? → несколько воркеров читают один канал (см. workerpool).

Решение на Go (merge.go):

// Package fanin — слияние N каналов в один (fan-in).
package fanin

import (
	"context"
	"sync"
)

// Merge читает из всех входных каналов и пишет в один выходной.
// Выход закрывается, когда закрыты все входы или отменён ctx.
func Merge[T any](ctx context.Context, chans ...<-chan T) <-chan T {
	out := make(chan T)
	var wg sync.WaitGroup
	wg.Add(len(chans))
	for _, ch := range chans {
		go func() {
			defer wg.Done()
			for v := range ch {
				select {
				case out <- v:
				case <-ctx.Done():
					return
				}
			}
		}()
	}
	go func() { wg.Wait(); close(out) }()
	return out
}

// MergeSelect — вариант для ровно двух каналов без доп. горутин: nil-канал в select блокируется навсегда,
// поэтому закрытый канал «выключаем», присваивая nil.
func MergeSelect[T any](a, b <-chan T) <-chan T {
	out := make(chan T)
	go func() {
		defer close(out)
		for a != nil || b != nil {
			select {
			case v, ok := <-a:
				if !ok {
					a = nil
					continue
				}
				out <- v
			case v, ok := <-b:
				if !ok {
					b = nil
					continue
				}
				out <- v
			}
		}
	}()
	return out
}

Pipeline (конвейер) на каналах

Уровень: Middle

Правила хорошей стадии

  1. Стадия владеет своим выходным каналом: создаёт и закрывает (defer close(out)).
  2. Читает вход через range — завершается, когда вход закрыт.
  3. Каждая отправка — в select с ctx.Done(): потребитель может уйти раньше.

Это ровно паттерн из статьи Go Concurrency Patterns: Pipelines and cancellation — на собесе полезно на неё сослаться.

Вопросы

  • Буферизированные или нет? — Небуферизированные дают backpressure; буфер сглаживает неравномерную скорость стадий, но не решает проблему медленного потребителя.
  • Как распараллелить медленную стадию? — fan-out на N воркеров + fan-in.

Решение на Go (pipeline.go):

// Package pipeline — конвейер из стадий, соединённых каналами, с корректной отменой.
package pipeline

import "context"

// Generate отдаёт числа в канал.
func Generate(ctx context.Context, nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		defer close(out)
		for _, n := range nums {
			select {
			case out <- n:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

// Map — универсальная стадия конвейера.
func Map[In, Out any](ctx context.Context, in <-chan In, fn func(In) Out) <-chan Out {
	out := make(chan Out)
	go func() {
		defer close(out)
		for v := range in {
			select {
			case out <- fn(v):
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

// Filter пропускает только элементы, удовлетворяющие pred.
func Filter[T any](ctx context.Context, in <-chan T, pred func(T) bool) <-chan T {
	out := make(chan T)
	go func() {
		defer close(out)
		for v := range in {
			if !pred(v) {
				continue
			}
			select {
			case out <- v:
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

Первый успешный ответ из N реплик (с таймаутом)

Уровень: Middle · Формулировка: «Есть 3 реплики сервиса. Верните первый ответ, но не дольше 100 мс».

Ключевой момент — утечка горутин

Если канал небуферизированный, то после return победителя остальные горутины навсегда заблокируются на ch <- res → goroutine leak. Решения: буфер len(fns) или select с ctx.Done() при отправке.

Что ещё показать

  • context.WithTimeout снаружи, cancel() через defer — отменяет проигравших.
  • errors.Join (Go 1.20+) — агрегирует ошибки, errors.Is работает по всем.
  • Hedged requests (Google, «The Tail at Scale»): второй запрос шлём не сразу, а через p95 задержки.

Решение на Go (first.go):

// Package firstresult — запрос к нескольким репликам, берём первый успешный ответ (hedged requests).
package firstresult

import (
	"context"
	"errors"
)

// ErrAllFailed возвращается, если ни одна реплика не ответила успешно.
var ErrAllFailed = errors.New("all replicas failed")

// First запускает все запросы параллельно и возвращает первый успешный результат.
// Остальные отменяются через ctx. Канал буферизирован на len(fns) — проигравшие горутины не утекут.
func First[T any](ctx context.Context, fns ...func(context.Context) (T, error)) (T, error) {
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	type res struct {
		v   T
		err error
	}
	ch := make(chan res, len(fns))
	for _, fn := range fns {
		go func() {
			v, err := fn(ctx)
			ch <- res{v, err}
		}()
	}
	var errs []error
	for range fns {
		select {
		case r := <-ch:
			if r.err == nil {
				return r.v, nil
			}
			errs = append(errs, r.err)
		case <-ctx.Done():
			var zero T
			return zero, ctx.Err()
		}
	}
	var zero T
	return zero, errors.Join(append([]error{ErrAllFailed}, errs...)...)
}

Кэш с TTL (in-memory)

Уровень: Middle · Где спрашивают: Wildberries, Ozon, Avito, Сбер

Разбор

  • map + sync.RWMutex: чтений больше, чем записей.
  • Двойная защита от протухших данных: ленивая проверка в Get + janitor по тикеру.
  • Close() через sync.Once — иначе janitor-горутина живёт вечно (утечка), а повторный close паникует.

Вопросы

  • Почему map не очищает память после delete? Бакеты не сжимаются. Для огромных кэшей периодически пересоздают map. (С Go 1.24 map реализован на Swiss Tables, но это поведение сохранилось.)
  • GC-нагрузка на миллионы записей с указателями? → шардирование, map[uint64]uint32 + байтовый буфер (подход bigcache/freecache).
  • sync.Map или map+RWMutex? sync.Map выигрывает при append-only или непересекающихся ключах у горутин; в остальных случаях map+mutex проще и часто быстрее.

Решение на Go (ttl.go):

// Package ttlcache — потокобезопасный кэш с TTL и фоновой очисткой.
package ttlcache

import (
	"sync"
	"time"
)

type item[V any] struct {
	val     V
	expires time.Time
}

// Cache — map + RWMutex + janitor-горутина.
type Cache[K comparable, V any] struct {
	mu    sync.RWMutex
	items map[K]item[V]
	now   func() time.Time
	stop  chan struct{}
	once  sync.Once
}

// New создаёт кэш; при cleanup > 0 запускает фоновую очистку. Не забудьте Close().
func New[K comparable, V any](cleanup time.Duration) *Cache[K, V] {
	c := &Cache[K, V]{items: make(map[K]item[V]), now: time.Now, stop: make(chan struct{})}
	if cleanup > 0 {
		go c.janitor(cleanup)
	}
	return c
}

// Set кладёт значение на ttl.
func (c *Cache[K, V]) Set(k K, v V, ttl time.Duration) {
	c.mu.Lock()
	c.items[k] = item[V]{v, c.now().Add(ttl)}
	c.mu.Unlock()
}

// Get возвращает значение, если оно не протухло (ленивая проверка — janitor может не успеть).
func (c *Cache[K, V]) Get(k K) (V, bool) {
	c.mu.RLock()
	it, ok := c.items[k]
	c.mu.RUnlock()
	if !ok || !c.now().Before(it.expires) {
		var zero V
		return zero, false
	}
	return it.val, true
}

// Len — число записей (включая ещё не вычищенные протухшие).
func (c *Cache[K, V]) Len() int {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return len(c.items)
}

// DeleteExpired удаляет протухшие записи.
func (c *Cache[K, V]) DeleteExpired() {
	now := c.now()
	c.mu.Lock()
	defer c.mu.Unlock()
	for k, it := range c.items { // удалять из map во время range — безопасно
		if !now.Before(it.expires) {
			delete(c.items, k)
		}
	}
}

func (c *Cache[K, V]) janitor(every time.Duration) {
	t := time.NewTicker(every)
	defer t.Stop()
	for {
		select {
		case <-t.C:
			c.DeleteExpired()
		case <-c.stop:
			return
		}
	}
}

// Close останавливает janitor. Идемпотентен.
func (c *Cache[K, V]) Close() { c.once.Do(func() { close(c.stop) }) }

Graceful shutdown HTTP-сервера

Уровень: Middle · Спрашивают на любом backend-собесе и в Kubernetes-контексте

Последовательность

  1. signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) — Kubernetes шлёт SIGTERM, ждёт terminationGracePeriodSeconds (30 с по умолчанию), потом SIGKILL.
  2. srv.Shutdown(ctx) — закрывает listener, ждёт активные запросы. Serve при этом сразу возвращает http.ErrServerClosed — это не ошибка.
  3. Затем закрываем зависимости в обратном порядке: воркеры → брокеры → БД.

Вопросы

  • Почему не srv.Close()? — рвёт активные соединения.
  • Readiness-проба? — при SIGTERM сначала переводим readiness в fail, ждём несколько секунд, пока балансировщик уберёт под, и только потом Shutdown.
  • context.WithoutCancel (Go 1.21+) — запросы не отменяются мгновенно при отмене корневого ctx.
  • ReadHeaderTimeout — без него сервер уязвим к Slowloris (линтер gosec G112).

Решение на Go (server.go):

// Package gracefulshutdown — HTTP-сервер с корректным завершением по SIGINT/SIGTERM.
package gracefulshutdown

import (
	"context"
	"errors"
	"net"
	"net/http"
	"time"
)

// Serve обслуживает ln, пока не отменён ctx (например, signal.NotifyContext),
// затем даёт активным запросам до timeout на завершение.
func Serve(ctx context.Context, ln net.Listener, h http.Handler, timeout time.Duration) error {
	srv := &http.Server{
		Handler:           h,
		ReadHeaderTimeout: 5 * time.Second, // защита от Slowloris — любимый вопрос
		BaseContext:       func(net.Listener) context.Context { return context.WithoutCancel(ctx) },
	}
	errCh := make(chan error, 1)
	go func() { errCh <- srv.Serve(ln) }()

	select {
	case err := <-errCh:
		return err // сервер упал сам
	case <-ctx.Done():
	}

	shCtx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()
	if err := srv.Shutdown(shCtx); err != nil { // перестаёт принимать новые, ждёт активные
		return err
	}
	if err := <-errCh; !errors.Is(err, http.ErrServerClosed) {
		return err
	}
	return nil
}

/*
Использование в main:

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	ln, _ := net.Listen("tcp", ":8080")
	if err := gracefulshutdown.Serve(ctx, ln, mux, 15*time.Second); err != nil {
		log.Fatal(err)
	}
*/

Параллельные запросы с лимитом и отменой (свой errgroup)

Уровень: Middle/Senior · Формулировка на собесе: «Есть 1000 URL. Скачайте их, не более 10 одновременно. При первой ошибке — остановитесь».

Что должно быть в ответе

  1. Семафор (chan struct{} ёмкостью N) — ограничение параллелизма.
  2. context.WithCancelCause (Go 1.20+) — отмена всех по первой ошибке с причиной.
  3. sync.Once для фиксации первой ошибки.
  4. Сохранение порядка: пишем в out[i] — разные индексы слайса не конфликтуют (data race нет).
  5. В проде — errgroup.WithContext + g.SetLimit(10).

Частые ошибки

  • Запуск 1000 горутин, а лимит — внутри горутины → память/файловые дескрипторы.
  • append в общий слайс из горутин без мьютекса → data race (ловится go test -race).
  • Игнорирование ctx внутри fn → отмена ничего не отменяет.

Решение на Go (parallel.go):

// Package parallel — параллельная обработка с лимитом и отменой при первой ошибке (как errgroup.SetLimit).
package parallel

import (
	"context"
	"sync"
)

// ForEach вызывает fn для каждого элемента, не более limit одновременно.
// При первой ошибке контекст отменяется, новые задачи не стартуют, возвращается первая ошибка.
func ForEach[T any](ctx context.Context, limit int, items []T, fn func(context.Context, T) error) error {
	ctx, cancel := context.WithCancelCause(ctx)
	defer cancel(nil)

	sem := make(chan struct{}, max(limit, 1))
	var (
		wg       sync.WaitGroup
		once     sync.Once
		firstErr error
	)
	for _, it := range items {
		select {
		case sem <- struct{}{}:
		case <-ctx.Done():
		}
		if ctx.Err() != nil {
			break
		}
		wg.Add(1)
		go func() {
			defer func() { <-sem; wg.Done() }()
			if err := fn(ctx, it); err != nil {
				once.Do(func() { firstErr = err; cancel(err) })
			}
		}()
	}
	wg.Wait()
	if firstErr != nil {
		return firstErr
	}
	return context.Cause(ctx) // ошибка родительского ctx, если его отменили извне
}

// Map — то же, но собирает результаты в порядке входа (каждая горутина пишет в свой индекс — гонки нет).
func Map[T, R any](ctx context.Context, limit int, items []T, fn func(context.Context, T) (R, error)) ([]R, error) {
	out := make([]R, len(items))
	idx := make([]int, len(items))
	for i := range idx {
		idx[i] = i
	}
	err := ForEach(ctx, limit, idx, func(ctx context.Context, i int) error {
		v, err := fn(ctx, items[i])
		out[i] = v
		return err
	})
	if err != nil {
		return nil, err
	}
	return out, nil
}

Rate Limiter (token bucket)

Уровень: Middle/Senior · Где спрашивают: Ozon, Avito, Яндекс, Т-Банк; часто как часть system design

Алгоритмы — знать все четыре

Алгоритм Суть Плюсы / минусы
Fixed window счётчик на интервал просто; всплеск ×2 на границе окон
Sliding log храним таймстемпы точно; память O(лимит)
Sliding window counter взвешенная сумма двух окон компромисс, популярен в Redis
Token bucket токены капают со скоростью r, ёмкость b допускает всплески, O(1) памяти
Leaky bucket очередь с постоянной скоростью вытекания сглаживает трафик

Разбор реализации

  • Ленивое пополнение: при каждом вызове tokens += elapsed * rate. Никаких фоновых тикеров.
  • Часы инъектируются (now func() time.Time) → детерминированные тесты. Альтернатива на Go 1.25+ — пакет testing/synctest с фейковым временем.
  • В проде: golang.org/x/time/rate. Распределённо: Redis + Lua (атомарно) или GCRA.

Решение на Go (bucket.go):

// Package ratelimiter — rate limiter по алгоритму token bucket.
package ratelimiter

import (
	"context"
	"sync"
	"time"
)

// Bucket пополняется со скоростью rate токенов/сек до ёмкости burst.
// Токены считаются «лениво» при каждом вызове — без фоновых горутин и тикеров.
type Bucket struct {
	mu     sync.Mutex
	rate   float64
	burst  float64
	tokens float64
	last   time.Time
	now    func() time.Time // подменяется в тестах
}

// New создаёт полный bucket.
func New(rate float64, burst int) *Bucket {
	return &Bucket{rate: rate, burst: float64(burst), tokens: float64(burst), last: time.Now(), now: time.Now}
}

func (b *Bucket) refill() {
	now := b.now()
	b.tokens = min(b.burst, b.tokens+now.Sub(b.last).Seconds()*b.rate)
	b.last = now
}

// Allow пытается забрать токен без ожидания.
func (b *Bucket) Allow() bool {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.refill()
	if b.tokens >= 1 {
		b.tokens--
		return true
	}
	return false
}

// Wait блокируется, пока не появится токен или не отменят ctx.
func (b *Bucket) Wait(ctx context.Context) error {
	for {
		b.mu.Lock()
		b.refill()
		if b.tokens >= 1 {
			b.tokens--
			b.mu.Unlock()
			return nil
		}
		wait := time.Duration((1 - b.tokens) / b.rate * float64(time.Second))
		b.mu.Unlock()

		t := time.NewTimer(wait)
		select {
		case <-ctx.Done():
			t.Stop()
			return ctx.Err()
		case <-t.C:
		}
	}
}

Батчер: пачка по размеру или по времени

Уровень: Middle/Senior · Контекст: запись в ClickHouse/Kafka/БД пачками, агрегация метрик

Условие

Из канала событий формировать пачки: отправлять, когда набралось N или прошло T с первого события в пачке. При закрытии входа — отправить остаток.

Ловушки

  • Переиспользование буфера после отправки (buf = buf[:0]) → получатель видит, как его пачку перезаписывают. Нужен новый слайс.
  • Таймер: time.NewTimer + Reset, а не time.After в цикле (до Go 1.23 это была утечка таймеров до срабатывания; с 1.23 таймеры собираются GC, но создавать таймер на каждую итерацию всё равно расточительно).
  • С Go 1.23 каналы таймеров синхронные (небуферизированные), Reset/Stop больше не требуют «вычерпывания» канала. В Go 1.27 GODEBUG asynctimerchan удалён окончательно.
  • Потеря остатка при закрытии входа.

Решение на Go (batcher.go):

// Package batcher — группировка событий в пачки по размеру ИЛИ по таймауту (как в Kafka producer / ClickHouse insert).
package batcher

import (
	"context"
	"time"
)

// Batch читает in и отдаёт пачки: когда набралось size элементов или прошло interval с первого элемента пачки.
// При закрытии in или отмене ctx остаток сбрасывается (flush), затем выход закрывается.
func Batch[T any](ctx context.Context, in <-chan T, size int, interval time.Duration) <-chan []T {
	out := make(chan []T)
	go func() {
		defer close(out)
		buf := make([]T, 0, size)
		timer := time.NewTimer(interval)
		timer.Stop()

		flush := func() bool {
			if len(buf) == 0 {
				return true
			}
			select {
			case out <- buf:
				buf = make([]T, 0, size) // новый слайс: старый теперь принадлежит получателю
				return true
			case <-ctx.Done():
				return false
			}
		}

		for {
			select {
			case v, ok := <-in:
				if !ok {
					timer.Stop()
					flush()
					return
				}
				if len(buf) == 0 {
					timer.Reset(interval) // окно отсчитывается от первого элемента пачки
				}
				buf = append(buf, v)
				if len(buf) == size {
					timer.Stop()
					if !flush() {
						return
					}
				}
			case <-timer.C:
				if !flush() {
					return
				}
			case <-ctx.Done():
				return
			}
		}
	}()
	return out
}

Pub/Sub брокер в памяти

Уровень: Middle/Senior

Дизайн-решения, которые надо проговорить

Вопрос Выбор здесь Альтернатива
Медленный подписчик drop (неблокирующий select default) блокироваться; отключать; кольцевой буфер
Гонка Publish vs Unsubscribe RWMutex: publish под RLock, закрытие под Lock горутина-владелец с каналами команд
Повторная отписка sync.Once флаг под мьютексом

Главная ловушка: отправка в закрытый канал → panic. Поэтому канал закрывает только брокер и только под эксклюзивной блокировкой.

Решение на Go (broker.go):

// Package pubsub — in-memory брокер сообщений: подписка, отписка, неблокирующая публикация.
package pubsub

import "sync"

// Broker рассылает сообщения всем подписчикам.
// Медленный подписчик не тормозит остальных: если его буфер полон, сообщение для него отбрасывается.
type Broker[T any] struct {
	mu      sync.RWMutex
	subs    map[chan T]struct{}
	closed  bool
	dropped int
}

// New создаёт брокер.
func New[T any]() *Broker[T] { return &Broker[T]{subs: make(map[chan T]struct{})} }

// Subscribe возвращает канал сообщений и функцию отписки (идемпотентную).
func (b *Broker[T]) Subscribe(buf int) (<-chan T, func()) {
	ch := make(chan T, buf)
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.closed {
		close(ch)
		return ch, func() {}
	}
	b.subs[ch] = struct{}{}
	var once sync.Once
	return ch, func() {
		once.Do(func() {
			b.mu.Lock()
			defer b.mu.Unlock()
			if _, ok := b.subs[ch]; ok {
				delete(b.subs, ch)
				close(ch)
			}
		})
	}
}

// Publish отправляет msg всем подписчикам без блокировки. Возвращает число доставок.
func (b *Broker[T]) Publish(msg T) int {
	b.mu.RLock()
	defer b.mu.RUnlock() // держим RLock, чтобы никто не закрыл канал во время отправки
	if b.closed {
		return 0
	}
	n := 0
	for ch := range b.subs {
		select {
		case ch <- msg:
			n++
		default: // буфер полон — drop (альтернативы: блокироваться, отключать подписчика)
		}
	}
	return n
}

// Close закрывает все каналы подписчиков.
func (b *Broker[T]) Close() {
	b.mu.Lock()
	defer b.mu.Unlock()
	if b.closed {
		return
	}
	b.closed = true
	for ch := range b.subs {
		close(ch)
		delete(b.subs, ch)
	}
}

Singleflight — защита от «эффекта стада» (cache stampede)

Уровень: Senior · Где спрашивают: Яндекс, Ozon, VK, высоконагруженные команды

Проблема

Ключ протух в кэше → 10 000 RPS одновременно идут в БД за одним и тем же. БД ложится.

Решение

Первый запрос по ключу выполняет работу, остальные ждут его результата на sync.WaitGroup. В проде — golang.org/x/sync/singleflight (есть DoChan и Forget).

Вопросы вдогонку

  • Что если fn зависла? → DoChan + select с таймаутом; Forget(key).
  • Паника в fn? → defer должен разбудить ждущих (в оригинале паника пробрасывается всем).
  • Чем дополнить? → probabilistic early expiration (XFetch), stale-while-revalidate, jitter TTL.

Реальный баг, пойманный go test -race при написании этого решения: счётчик dup инкрементируется под мьютексом, а читался без него. Поэтому конкурентный код всегда проверяйте с -race.

Решение на Go (singleflight.go):

// Package singleflight — схлопывание одновременных одинаковых запросов (защита от cache stampede).
package singleflight

import "sync"

type call[V any] struct {
	wg  sync.WaitGroup
	val V
	err error
	dup int
}

// Group — generic-аналог golang.org/x/sync/singleflight.
type Group[K comparable, V any] struct {
	mu sync.Mutex
	m  map[K]*call[V]
}

// Do выполняет fn один раз для всех одновременных вызовов с одним ключом.
// shared == true, если результат получен от чужого вызова.
func (g *Group[K, V]) Do(key K, fn func() (V, error)) (v V, err error, shared bool) {
	g.mu.Lock()
	if g.m == nil {
		g.m = make(map[K]*call[V])
	}
	if c, ok := g.m[key]; ok {
		c.dup++
		g.mu.Unlock()
		c.wg.Wait()
		return c.val, c.err, true
	}
	c := &call[V]{}
	c.wg.Add(1)
	g.m[key] = c
	g.mu.Unlock()

	defer func() { // даже если fn паникует, ждущие не должны висеть вечно
		g.mu.Lock()
		delete(g.m, key)
		shared = c.dup > 0 // dup меняется под g.mu — читаем тоже под ним (иначе data race)
		g.mu.Unlock()
		c.wg.Done()
	}()
	c.val, c.err = fn()
	return c.val, c.err, false // shared выставит defer
}

Circuit Breaker на Go

Уровень: Senior · Где спрашивают: Ozon, Авито, Т-Банк, VK — как часть разговора про отказоустойчивость

Условие

Реализуйте circuit breaker: после N ошибок подряд он «размыкается» и сразу возвращает ErrOpen; через timeout пропускает ровно один пробный запрос; успех — замыкается, ошибка — снова размыкается.

Ключевые моменты

  • Состояния Closed → Open → Half-Open — нарисуйте автомат перед кодом, это оценивают.
  • Сам вызов fn() — вне мьютекса, иначе breaker сериализует весь трафик.
  • В Half-Open пускаем один пробный запрос (флаг probing), остальные получают ErrOpen — иначе после восстановления на лежащий сервис хлынет весь накопленный поток.
  • Часы инъектируются (now func() time.Time) → тесты без time.Sleep.

Follow-up

  • Порог по доле ошибок в скользящем окне вместо «N подряд» (как в sony/gobreaker — ReadyToTrip(counts)).
  • Какие ошибки считать? context.Canceled клиента и 4xx — не вина зависимости, их не считают.
  • Метрики переходов состояний, отдельный breaker на каждый хост/эндпоинт.
  • Чем отличается от rate limiter и bulkhead?

Решение на Go (breaker.go):

// Package breaker — Circuit Breaker с тремя состояниями: Closed → Open → Half-Open.
package breaker

import (
	"errors"
	"sync"
	"time"
)

var ErrOpen = errors.New("circuit breaker is open")

type State int

const (
	Closed State = iota
	Open
	HalfOpen
)

type Breaker struct {
	mu          sync.Mutex
	state       State
	failures    int           // подряд идущие ошибки в Closed
	threshold   int           // сколько ошибок подряд открывают breaker
	openTimeout time.Duration // сколько ждать в Open перед пробой
	openedAt    time.Time
	probing     bool // в Half-Open пропускаем ровно один пробный запрос
	now         func() time.Time
}

func New(threshold int, openTimeout time.Duration) *Breaker {
	return &Breaker{threshold: threshold, openTimeout: openTimeout, now: time.Now}
}

// Do выполняет fn, если breaker пропускает запрос.
func (b *Breaker) Do(fn func() error) error {
	if err := b.before(); err != nil {
		return err
	}
	err := fn() // сам вызов — БЕЗ мьютекса, иначе сериализуем все запросы
	b.after(err)
	return err
}

func (b *Breaker) before() error {
	b.mu.Lock()
	defer b.mu.Unlock()
	switch b.state {
	case Open:
		if b.now().Sub(b.openedAt) < b.openTimeout {
			return ErrOpen
		}
		b.state = HalfOpen
		fallthrough
	case HalfOpen:
		if b.probing {
			return ErrOpen // проба уже идёт — остальных не пускаем
		}
		b.probing = true
	}
	return nil
}

func (b *Breaker) after(err error) {
	b.mu.Lock()
	defer b.mu.Unlock()
	switch b.state {
	case HalfOpen:
		b.probing = false
		if err != nil {
			b.trip()
			return
		}
		b.state, b.failures = Closed, 0
	case Closed:
		if err == nil {
			b.failures = 0
			return
		}
		b.failures++
		if b.failures >= b.threshold {
			b.trip()
		}
	}
}

func (b *Breaker) trip() {
	b.state = Open
	b.openedAt = b.now()
	b.failures = 0
}

func (b *Breaker) State() State {
	b.mu.Lock()
	defer b.mu.Unlock()
	return b.state
}
Тесты (`breaker_test.go`)
package breaker

import (
	"errors"
	"sync"
	"testing"
	"time"
)

func TestBreaker(t *testing.T) {
	now := time.Unix(0, 0)
	b := New(3, time.Second)
	b.now = func() time.Time { return now }
	fail := errors.New("fail")

	for i := 0; i < 3; i++ {
		_ = b.Do(func() error { return fail })
	}
	if b.State() != Open {
		t.Fatal("want Open")
	}
	if err := b.Do(func() error { return nil }); !errors.Is(err, ErrOpen) {
		t.Fatalf("want ErrOpen, got %v", err)
	}
	now = now.Add(2 * time.Second)
	_ = b.Do(func() error { return fail }) // проба неудачна
	if b.State() != Open {
		t.Fatal("want Open after failed probe")
	}
	now = now.Add(2 * time.Second)
	if err := b.Do(func() error { return nil }); err != nil {
		t.Fatal(err)
	}
	if b.State() != Closed {
		t.Fatal("want Closed")
	}
}

func TestSingleProbe(t *testing.T) {
	now := time.Unix(0, 0)
	b := New(1, time.Second)
	b.now = func() time.Time { return now }
	_ = b.Do(func() error { return errors.New("x") })
	now = now.Add(2 * time.Second)

	release := make(chan struct{})
	started := make(chan struct{})
	go b.Do(func() error { close(started); <-release; return nil })
	<-started
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := b.Do(func() error { return nil }); !errors.Is(err, ErrOpen) {
				t.Error("second probe must be rejected")
			}
		}()
	}
	wg.Wait()
	close(release)
}

Consistent hashing с виртуальными узлами

Уровень: Senior · Где спрашивают: Яндекс, VK, Avito — шардирование кэшей и system design

Условие

Реализуйте кольцо consistent hashing: Add(nodes...), Remove(node), Get(key) node. При удалении узла должны переехать только ключи этого узла.

Разбор

  • Каждый физический узел ставится на кольцо replicas раз (hash("i#node")) — виртуальные узлы выравнивают распределение.
  • Кольцо — отсортированный слайс хэшей; поиск владельца — slices.BinarySearch за O(log n), при выходе за конец — переход на 0 (замыкание кольца).
  • Чтений намного больше, чем изменений топологии → RWMutex (или copy-on-write через atomic.Pointer).

Follow-up

  • Коллизии хэшей виртуальных узлов? (перезапишут owner — нужна проверка или 64-битный хэш, например xxhash).
  • Как учесть веса узлов? (число виртуальных узлов пропорционально весу).
  • Репликация: взять N различных физических узлов, идя по кольцу дальше.
  • Rendezvous hashing и Jump Hash — когда они лучше.

Решение на Go (ring.go):

// Package ring — consistent hashing с виртуальными узлами.
package ring

import (
	"hash/crc32"
	"slices"
	"strconv"
	"sync"
)

type Ring struct {
	mu       sync.RWMutex
	replicas int               // виртуальных узлов на физический
	hashes   []uint32          // отсортированное кольцо
	owner    map[uint32]string // хэш → физический узел
}

func New(replicas int) *Ring {
	return &Ring{replicas: replicas, owner: make(map[uint32]string)}
}

func hash(s string) uint32 { return crc32.ChecksumIEEE([]byte(s)) }

func (r *Ring) Add(nodes ...string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	for _, n := range nodes {
		for i := range r.replicas {
			h := hash(strconv.Itoa(i) + "#" + n)
			r.owner[h] = n
			r.hashes = append(r.hashes, h)
		}
	}
	slices.Sort(r.hashes)
}

func (r *Ring) Remove(node string) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.hashes = slices.DeleteFunc(r.hashes, func(h uint32) bool {
		if r.owner[h] == node {
			delete(r.owner, h)
			return true
		}
		return false
	})
}

// Get возвращает узел, отвечающий за key: первый виртуальный узел по часовой стрелке.
func (r *Ring) Get(key string) (string, bool) {
	r.mu.RLock()
	defer r.mu.RUnlock()
	if len(r.hashes) == 0 {
		return "", false
	}
	h := hash(key)
	i, _ := slices.BinarySearch(r.hashes, h)
	if i == len(r.hashes) {
		i = 0 // замыкаем кольцо
	}
	return r.owner[r.hashes[i]], true
}
Тесты (`ring_test.go`)
package ring

import (
	"strconv"
	"testing"
)

func TestRingMovesFewKeys(t *testing.T) {
	r := New(100)
	r.Add("a", "b", "c", "d")
	before := map[string]string{}
	for i := range 10000 {
		k := "key" + strconv.Itoa(i)
		before[k], _ = r.Get(k)
	}
	r.Remove("d")
	moved := 0
	for k, n := range before {
		got, _ := r.Get(k)
		if got != n {
			if n != "d" {
				t.Fatalf("key %s moved from live node %s", k, n)
			}
			moved++
		}
	}
	if moved == 0 || moved > 4000 {
		t.Fatalf("unexpected moved=%d", moved)
	}
	t.Logf("moved %d of 10000 keys", moved)
}

Шардированная конкурентная map (generics)

Уровень: Middle/Senior · Где спрашивают: Ozon, Wildberries, биржи — «sync.Map медленная, сделайте быстрее»

Условие

Сделайте потокобезопасную generic-map, которая масштабируется лучше, чем map + один RWMutex, при записи из многих горутин. Нужна атомарная операция «прочитать-изменить-записать».

Разбор

  • N шардов, у каждого своя блокировка; шард выбирается по хэшу ключа. Конкуренция падает примерно в N раз.
  • Хэш произвольного comparable ключа — maphash.Comparable (Go 1.24) со случайным seed (защита от hash flooding).
  • Число шардов — степень двойки → hash & mask вместо %.
  • Compute решает классическую ошибку v, _ := m.Load(k); m.Store(k, v+1) — это race condition без data race: две горутины прочитают одно значение.
  • Padding между шардами против false sharing — соседние мьютексы не должны жить в одной кэш-линии.

Follow-up

  • Когда sync.Map всё же лучше? (append-only кэши, непересекающиеся ключи).
  • Как сделать Range без блокировки всех шардов сразу? (по одному шарду; согласованного снимка не будет — проговорите).
  • Как измерить выигрыш? (b.RunParallel + -cpu=1,4,16, профиль mutex).

Решение на Go (shardmap.go):

// Package shardmap — конкурентная generic-map с шардированием блокировок.
package shardmap

import (
	"hash/maphash"
	"sync"
)

type shard[K comparable, V any] struct {
	mu sync.RWMutex
	m  map[K]V
	_  [64]byte // padding: соседние шарды не делят кэш-линию (false sharing)
}

type Map[K comparable, V any] struct {
	seed   maphash.Seed
	shards []shard[K, V]
	mask   uint64
}

// New создаёт map с n шардами; n округляется вверх до степени двойки.
func New[K comparable, V any](n int) *Map[K, V] {
	size := 1
	for size < n {
		size <<= 1
	}
	m := &Map[K, V]{seed: maphash.MakeSeed(), shards: make([]shard[K, V], size), mask: uint64(size - 1)}
	for i := range m.shards {
		m.shards[i].m = make(map[K]V)
	}
	return m
}

func (m *Map[K, V]) shard(k K) *shard[K, V] {
	return &m.shards[maphash.Comparable(m.seed, k)&m.mask] // maphash.Comparable — Go 1.24
}

func (m *Map[K, V]) Load(k K) (V, bool) {
	s := m.shard(k)
	s.mu.RLock()
	defer s.mu.RUnlock()
	v, ok := s.m[k]
	return v, ok
}

func (m *Map[K, V]) Store(k K, v V) {
	s := m.shard(k)
	s.mu.Lock()
	s.m[k] = v
	s.mu.Unlock()
}

// Compute атомарно обновляет значение по ключу (read-modify-write под одной блокировкой).
func (m *Map[K, V]) Compute(k K, fn func(old V, ok bool) V) V {
	s := m.shard(k)
	s.mu.Lock()
	defer s.mu.Unlock()
	old, ok := s.m[k]
	nv := fn(old, ok)
	s.m[k] = nv
	return nv
}

func (m *Map[K, V]) Len() int {
	n := 0
	for i := range m.shards {
		s := &m.shards[i]
		s.mu.RLock()
		n += len(s.m)
		s.mu.RUnlock()
	}
	return n
}
Тесты (`shardmap_test.go`)
package shardmap

import (
	"sync"
	"testing"
)

func TestConcurrentCompute(t *testing.T) {
	m := New[string, int](16)
	var wg sync.WaitGroup
	for range 50 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for range 1000 {
				m.Compute("hits", func(old int, _ bool) int { return old + 1 })
			}
		}()
	}
	wg.Wait()
	if v, _ := m.Load("hits"); v != 50000 {
		t.Fatalf("got %d", v)
	}
	if m.Len() != 1 {
		t.Fatal("len")
	}
}

Lock-free стек Трайбера на atomic.Pointer

Уровень: Senior · Где спрашивают: HFT/биржи, инфраструктурные команды, вопрос про ABA

Условие

Реализуйте стек без мьютексов, безопасный для конкурентных Push/Pop.

Разбор

  • head — atomic.Pointer[node[T]]; обе операции — CAS-цикл: прочитать head, подготовить новое значение, CompareAndSwap, при неудаче повторить.
  • Узел до публикации принадлежит только нам, поэтому n.next = old писать можно без атомиков; CAS публикует его с гарантией happens-before.
  • ABA: в C++ после Pop узел могли бы освободить и выделить заново по тому же адресу. В Go, пока мы держим old, GC не переиспользует его память — ABA невозможна.

Follow-up

  • Будет ли это быстрее []T + Mutex? Часто нет: все горутины бьются в одну кэш-линию head, плюс аллокация на каждый Push. Покажите бенчмарк.
  • Что такое lock-free vs wait-free? (система в целом прогрессирует vs каждая операция завершается за конечное число шагов).
  • Lock-free очередь Майкла–Скотта — чем сложнее?

Решение на Go (lfstack.go):

// Package lfstack — lock-free стек Трайбера на atomic.Pointer.
package lfstack

import "sync/atomic"

type node[T any] struct {
	val  T
	next *node[T]
}

type Stack[T any] struct {
	head atomic.Pointer[node[T]]
}

func (s *Stack[T]) Push(v T) {
	n := &node[T]{val: v}
	for {
		old := s.head.Load()
		n.next = old
		if s.head.CompareAndSwap(old, n) {
			return
		}
		// кто-то успел изменить head — повторяем (CAS-loop)
	}
}

func (s *Stack[T]) Pop() (T, bool) {
	for {
		old := s.head.Load()
		if old == nil {
			var zero T
			return zero, false
		}
		// ABA здесь не страшна: пока мы держим old, GC не переиспользует его память,
		// поэтому тот же адрес не может «вернуться» другим узлом.
		if s.head.CompareAndSwap(old, old.next) {
			return old.val, true
		}
	}
}
Тесты (`lfstack_test.go`)
package lfstack

import (
	"sync"
	"sync/atomic"
	"testing"
)

func TestStack(t *testing.T) {
	var s Stack[int]
	var wg sync.WaitGroup
	const G, N = 8, 10000
	for g := range G {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for i := range N {
				s.Push(g*N + i)
			}
		}()
	}
	wg.Wait()
	var popped atomic.Int64
	seen := make([]atomic.Bool, G*N)
	for range G {
		wg.Add(1)
		go func() {
			defer wg.Done()
			for {
				v, ok := s.Pop()
				if !ok {
					return
				}
				if seen[v].Swap(true) {
					t.Errorf("dup %d", v)
				}
				popped.Add(1)
			}
		}()
	}
	wg.Wait()
	if popped.Load() != G*N {
		t.Fatalf("popped %d", popped.Load())
	}
}

Retry с экспоненциальной задержкой и jitter

Уровень: Middle/Senior · Где спрашивают: везде, где есть внешние вызовы; часто в паре с circuit breaker

Условие

Напишите Do(ctx, policy, fn): повторяет fn до Attempts раз с экспоненциальной задержкой и jitter, прекращает при отмене контекста и не повторяет «постоянные» ошибки.

Ключевые моменты

  • Full jitter: sleep = rand[0, min(Max, Base·2ⁿ)) — разносит ретраи клиентов во времени.
  • Ожидание — через select с ctx.Done(), а не time.Sleep: отмена срабатывает сразу.
  • Постоянные ошибки (валидация, 4xx) оборачиваются в Permanent(err) и не повторяются.
  • Защита от переполнения Base << attempt.
  • Итоговая ошибка содержит и последнюю ошибку, и причину отмены (errors.Join + context.Cause).

Follow-up

  • Retry budget / token bucket на ретраи, чтобы не устроить retry storm.
  • Учесть Retry-After от сервера.
  • Какие операции нельзя повторять без idempotency key?
  • Как протестировать без реальных задержек? (инъекция sleep, testing/synctest в Go 1.25+).

Решение на Go (retry.go):

// Package retry — повтор с экспоненциальной задержкой, full jitter и уважением к context.
package retry

import (
	"context"
	"errors"
	"math/rand/v2"
	"time"
)

type Policy struct {
	Attempts int           // всего попыток, включая первую
	Base     time.Duration // базовая задержка
	Max      time.Duration // потолок задержки
}

// permanentError — ошибка, которую бессмысленно повторять (400, валидация и т.п.).
type permanentError struct{ err error }

func (p permanentError) Error() string { return p.err.Error() }
func (p permanentError) Unwrap() error { return p.err }

func Permanent(err error) error { return permanentError{err} }

func Do(ctx context.Context, p Policy, fn func(context.Context) error) error {
	var err error
	for attempt := 0; attempt < p.Attempts; attempt++ {
		if err = fn(ctx); err == nil {
			return nil
		}
		var perm permanentError
		if errors.As(err, &perm) {
			return perm.err
		}
		if attempt == p.Attempts-1 {
			break
		}
		// full jitter: sleep = rand[0, min(Max, Base*2^attempt))
		backoff := p.Max
		if b := p.Base << attempt; attempt < 32 && b > 0 && b < p.Max {
			backoff = b // защищаемся от переполнения сдвига
		}
		sleep := rand.N(backoff) + 1
		t := time.NewTimer(sleep)
		select {
		case <-ctx.Done():
			t.Stop()
			return errors.Join(err, context.Cause(ctx))
		case <-t.C:
		}
	}
	return err
}
Тесты (`retry_test.go`)
package retry

import (
	"context"
	"errors"
	"testing"
	"time"
)

var errTmp = errors.New("temporary")

func TestRetrySucceeds(t *testing.T) {
	n := 0
	err := Do(context.Background(), Policy{5, time.Millisecond, 10 * time.Millisecond}, func(context.Context) error {
		n++
		if n < 3 {
			return errTmp
		}
		return nil
	})
	if err != nil || n != 3 {
		t.Fatalf("err=%v n=%d", err, n)
	}
}

func TestPermanent(t *testing.T) {
	n := 0
	bad := errors.New("bad request")
	err := Do(context.Background(), Policy{5, time.Millisecond, time.Millisecond}, func(context.Context) error {
		n++
		return Permanent(bad)
	})
	if !errors.Is(err, bad) || n != 1 {
		t.Fatalf("err=%v n=%d", err, n)
	}
}

func TestContextCancel(t *testing.T) {
	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
	defer cancel()
	err := Do(ctx, Policy{100, 50 * time.Millisecond, time.Second}, func(context.Context) error { return errTmp })
	if !errors.Is(err, context.DeadlineExceeded) || !errors.Is(err, errTmp) {
		t.Fatalf("err=%v", err)
	}
}

Чек-лист подготовки к собеседованию Go-разработчика

Отмечайте [x] по мере готовности (форкните репозиторий — и чек-лист станет вашим).

Язык

  • Zero values, new vs make, new(expr) (1.26)
  • Устройство слайса, рост cap, ловушки общего массива, a[i:j:k]
  • Устройство map (Swiss Tables с 1.24), почему нельзя &m[k], конкурентный доступ
  • Строки: байты vs руны, UTF-8, strings.Builder
  • Интерфейсы: iface/eface, nil-интерфейс, method sets
  • Встраивание vs наследование
  • defer: порядок, аргументы, именованные результаты
  • Ошибки: %w, errors.Is/As/AsType/Join
  • panic/recover, что нельзя поймать
  • Дженерики: constraints, ~, реализация (GC shape), generic-методы (1.27)
  • Итераторы iter.Seq, range-over-func

Конкурентность

  • Горутина vs поток, модель G-M-P, work stealing, вытеснение
  • Каналы: таблица поведения nil/закрытый/открытый
  • select, nil-каналы, default
  • Mutex (normal/starvation), RWMutex, Once, Cond, Pool, atomic
  • Memory model, happens-before, data race vs race condition
  • context: все конструкторы, WithCancelCause, WithoutCancel
  • Написать без подсказок: worker pool, fan-in, pipeline, rate limiter, errgroup
  • Утечки горутин и их поиск (pprof, goroutineleak, goleak, synctest)

Runtime и производительность

  • Escape analysis (-gcflags=-m)
  • GC: tri-color, write barrier, Green Tea, GOGC, GOMEMLIMIT
  • GOMAXPROCS в контейнерах (1.25)
  • pprof (cpu, heap, goroutine, mutex, block), trace, PGO
  • Бенчмарки с b.Loop(), benchstat

Senior+

  • Внутренности hchan, selectgo, таймеры Go 1.23, sysmon
  • Hybrid write barrier, mark assist, Swiss Tables изнутри
  • Инлайнинг, BCE, PGO, директивы компилятора, -gcflags=-m/-S
  • Правила unsafe.Pointer, unsafe.String/Slice, стоимость reflect и cgo
  • Утечки памяти, zero-alloc техники, runtime/metrics, off-CPU профилирование
  • CAS и ABA, copy-on-write через atomic.Pointer, unique/weak
  • MVS, GOPRIVATE, воспроизводимая сборка, govulncheck
  • Ретраи с jitter и бюджетом, circuit breaker, идемпотентность, саги, fencing tokens
  • Написать без подсказок: circuit breaker, consistent hashing, sharded map

Backend

  • net/http: ServeMux с 1.22, middleware, таймауты, graceful shutdown
  • gRPC vs REST, interceptors, дедлайны
  • database/sql/pgx: пул, транзакции, уровни изоляции, индексы
  • Kafka: партиции, consumer groups, семантики доставки, outbox
  • Redis: кэширование, инвалидация, распределённые локи
  • Observability: slog, Prometheus, OpenTelemetry

System Design

  • Алгоритм ответа, оценка нагрузки
  • CAP, репликация, шардирование, consistent hashing
  • 3+ задачи вслух: сокращатель ссылок, rate limiter, чат/лента

Алгоритмы

  • Two pointers, sliding window, hash map, stack, binary search
  • Heap, BFS/DFS, связные списки, деревья
  • Базовое DP, сортировки, оценка сложности

Полезные ресурсы

  • The Go Programming Language Specification
  • The Go Memory Model
  • Effective Go · Go Code Review Comments
  • Release notes Go · Go blog
  • Go Concurrency Patterns: Pipelines
  • 100 Go Mistakes and How to Avoid Them
  • Uber Go Style Guide
  • Go runtime: исходники runtime/chan.go, select.go, proc.go — лучший ответ на вопросы «как устроено»
  • A Guide to the Go Garbage Collector
  • Profile-guided optimization · Diagnostics
  • Swiss Tables в Go (блог)
  • The Tail at Scale (Dean, Barroso) · Exponential Backoff And Jitter (AWS)
  • Designing Data-Intensive Applications (Kleppmann) — распределённые системы для Senior

Участие

Были на Go-собеседовании? Поделитесь вопросом или отправьте Pull Request с исправлением или новой задачей.

Лицензия

MIT — используйте свободно, ссылка на репозиторий приветствуется.


Ключевые слова: собеседование Go, собеседование Golang, вопросы на собеседовании Go разработчика, Golang interview questions 2026, Go interview questions and answers, подготовка к собеседованию Golang, задачи Go собеседование, live coding Go, конкурентность Go, горутины и каналы вопросы, Go backend interview, Golang developer interview, Go junior middle senior, system design Go, Go senior interview, внутренности Go runtime, lock-free Go, circuit breaker Go, consistent hashing Go, Go 1.27, собеседование в Яндекс Ozon Авито Wildberries Go.