|
|
||
|---|---|---|
| README.md | ||
Go собеседование 2026: вопросы, ответы и задачи (Golang Interview Questions)
Полная подготовка к собеседованию на 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-awareGOMAXPROCS, 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
Если времени мало — начните с этих:
- Как устроен слайс и что делает
append? - Как устроена map? Почему она не потокобезопасна?
- Почему
err != nil, если вернули nil-указатель? - Value receiver vs pointer receiver
- Как работает
defer? - Горутина vs поток ОС
- Поведение nil и закрытых каналов
- Кто закрывает канал?
- Что такое goroutine leak и как найти?
- Модель G-M-P
- Как работает GC? Что такое Green Tea?
- Escape analysis
- Mutex vs RWMutex vs atomic
- Модель памяти и happens-before
- Зачем
contextи почему нуженcancel()? errors.Isvserrors.As- Поймает ли
recoverпанику из горутины? - Как реализованы дженерики?
- Напишите worker pool
- Что нового в последних версиях Go?
Топ-10 вопросов Senior-уровня
Если идёте на Senior/Lead:
- Как устроен канал внутри и что такое прямая передача между стеками?
- Как работает
selectи почему блокировки берутся по адресу канала? - Hybrid write barrier: почему паузы GC субмиллисекундные
- Swiss Tables в map: H1/H2, группы, расширяемое хэширование
- Инлайнинг, BCE и PGO
- Правила
unsafe.Pointerи zero-copy[]byte ↔ string - Утечки памяти при наличии GC
- Рост p99 при «нормальном» CPU-профиле
- Ретраи, circuit breaker и идемпотентность
- Распределённый лок и 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()? Порядок инициализации пакета
- Инициализируются импортированные пакеты (рекурсивно, каждый один раз).
- Переменные уровня пакета — в порядке зависимостей.
- Все
init()в порядке файлов (как их передалgo build, обычно по алфавиту) и порядке объявления. 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 соответствующего получения;
closehappens-before получения zero value; - для небуферизированного канала получение happens-before завершения отправки;
Unlockhappens-before следующегоLock;Once.Do(f): завершениеfhappens-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.
- Короткая STW-фаза: включить write barrier.
- Конкурентная маркировка: белые (не посещены) → серые (в очереди) → чёрные (обработаны). Горутины, много аллоцирующие, помогают GC (mark assist).
- Короткая STW: завершение маркировки.
- Конкурентная очистка (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— любой тип с underlyingint(например,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:
pollorder— случайная перестановка кейсов (поэтому выбор среди готовых случаен и нет голодания).lockorder— кейсы, отсортированные по адресу канала: все каналы блокируются в едином порядке, чтобы дваselectне устроили deadlock друг другу.- Проход по
pollorder: если что-то готово — выполняем и выходим. Естьdefault— выходим через него. - Иначе горутина ставит свой
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 }больше не нужен.
- таймеры и тикеры, на которые никто не ссылается, собираются GC даже без
- Вернуть старое поведение:
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, главное:
*T1→unsafe.Pointer→*T2(если T2 не больше T1 и раскладка совместима).unsafe.Pointer→uintptr— только для печати/сравнения; обратно превращать нельзя.- Арифметика
unsafe.Pointer(uintptr(p) + off)— в одном выражении. Нельзя сохранитьuintptrв переменную: для GC это просто число, объект могут собрать или (при росте стека) переместить. Лучшеunsafe.Add(p, off)(Go 1.17). uintptrв аргументахsyscall.Syscall— компилятор держит объект живым до конца вызова.- Результат
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)?
- Горутины, заблокированные навсегда (самая частая) — держат стек и всё, на что ссылаются.
- Подслайс/подстрока большого буфера:
small := big[:10]держит весьbig. Лечитьbytes.Clone,strings.Clone,slices.Clone. - Map не сжимается после
delete— периодически пересоздавать. - Кэши без лимита и TTL, глобальные map «на всякий случай».
time.TickerбезStopиtime.Afterв цикле — до Go 1.23 (см. вопрос про таймеры).- Циклы с
SetFinalizer— никогда не освобождаются (используйтеruntime.AddCleanup). - Память C через cgo — GC о ней не знает.
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.24crypto/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 минут)
- Требования (5–10 мин). Функциональные: что делает система. Нефункциональные: RPS, объём данных, латентность (p99), доступность, консистентность. Задавайте вопросы — это оценивают.
- Оценка нагрузки (back-of-the-envelope). 10 млн DAU × 10 запросов / 86 400 с ≈ 1 200 RPS, пик ×3–5.
- API — эндпоинты или gRPC-методы.
- Модель данных и выбор хранилища.
- Высокоуровневая схема: клиент → LB → сервисы → кэш → БД → очередь.
- Углубление в узкое место, которое выберет интервьюер.
- Масштабирование и отказы: шардирование, репликация, ретраи, деградация.
Цифры, которые надо помнить
| Операция | Время |
|---|---|
| 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 году
Типичные этапы
- Скрининг с рекрутером (15–30 мин) — опыт, ожидания, мотивация.
- Техническое интервью по Go (60–90 мин) — теория (этот репозиторий, разделы 01–10), «что выведет код» (tricky), небольшая задача на конкурентность.
- Алгоритмическая секция / live coding (45–60 мин) — 1–2 задачи уровня LeetCode Easy/Medium (раздел задач). Характерна для Яндекса, Google и крупных бигтехов.
- System design (60 мин) — для Middle+ и выше (раздел 14).
- Практическое/архитектурное задание или code review — найти баги в чужом коде (гонки, утечки, необработанные ошибки).
- Финальное интервью с командой / руководителем — поведенческие вопросы.
Что оценивают на 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.
Разбор
- Наивно — два вложенных цикла,
O(n²). Назовите это вслух и сразу предложите лучше. - Hash map — идём слева направо, для
vищемtarget-vсреди уже увиденных.O(n)время /O(n)память. - Массив отсортирован — два указателя,
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
Условие
Даны два неотсортированных слайса. Верните их пересечение.
Уточните у интервьюера (это оценивают!)
- Учитывать кратность? (
[2,2]∩[2]=[2]или[2,2]?) - Важен ли порядок?
- Какие размеры? Помещается ли в память?
Решения
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
Идея
Нужен канонический ключ анаграммы:
- Отсортированные руны слова —
O(L log L), работает с Unicode. - Массив счётчиков
[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.
Ключевые моменты, которые проверяют
- Кто закрывает канал результатов? Только отправитель и только после завершения всех отправителей → отдельная горутина
wg.Wait(); close(out). - Утечки горутин. Отправка в
outдолжна быть вselectсctx.Done(), иначе при уходе читателя воркер повиснет навсегда. wg.Addдо запуска горутины, не внутри неё (иначеWaitможет проскочить).- С Go 1.22 переменная цикла своя на каждой итерации — старый трюк
i := iне нужен. - С 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
Правила хорошей стадии
- Стадия владеет своим выходным каналом: создаёт и закрывает (
defer close(out)). - Читает вход через
range— завершается, когда вход закрыт. - Каждая отправка — в
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-контексте
Последовательность
signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)— Kubernetes шлёт SIGTERM, ждётterminationGracePeriodSeconds(30 с по умолчанию), потом SIGKILL.srv.Shutdown(ctx)— закрывает listener, ждёт активные запросы.Serveпри этом сразу возвращаетhttp.ErrServerClosed— это не ошибка.- Затем закрываем зависимости в обратном порядке: воркеры → брокеры → БД.
Вопросы
- Почему не
srv.Close()? — рвёт активные соединения. - Readiness-проба? — при SIGTERM сначала переводим readiness в fail, ждём несколько секунд, пока балансировщик уберёт под, и только потом
Shutdown. context.WithoutCancel(Go 1.21+) — запросы не отменяются мгновенно при отмене корневого ctx.ReadHeaderTimeout— без него сервер уязвим к Slowloris (линтерgosecG112).
Решение на 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 одновременно. При первой ошибке — остановитесь».
Что должно быть в ответе
- Семафор (
chan struct{}ёмкостью N) — ограничение параллелизма. context.WithCancelCause(Go 1.20+) — отмена всех по первой ошибке с причиной.sync.Onceдля фиксации первой ошибки.- Сохранение порядка: пишем в
out[i]— разные индексы слайса не конфликтуют (data race нет). - В проде —
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 GODEBUGasynctimerchanудалён окончательно. - Потеря остатка при закрытии входа.
Решение на 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,
newvsmake,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.