Полная подготовка к собеседованию на 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-уровня
- Полезные ресурсы
Если времени мало — начните с этих:
- Как устроен слайс и что делает
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?
Если идёте на 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
Раздел для Junior/Middle. Эти вопросы задают в первые 10 минут, чтобы понять, писали ли вы на Go по-настоящему.
- Компилируется в один статический бинарник, быстрый старт → удобно в Docker/Kubernetes.
- Встроенная конкурентность: горутины (стартовый стек ~2 КБ) + каналы, планировщик M:N в рантайме.
- Сборщик мусора с низкими паузами (concurrent mark-sweep; с Go 1.26 по умолчанию — Green Tea GC).
- Минималистичный синтаксис, один стиль (
gofmt), быстрая компиляция. - Сильная стандартная библиотека:
net/http,context,encoding/json,testing,log/slog. - Нет наследования — композиция через встраивание (embedding) и неявные интерфейсы.
Любая переменная без явной инициализации получает нулевое значение:
| Тип | 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.
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.
make— только для слайсов, map и каналов: инициализирует внутреннюю структуру и возвращает значение (не указатель).new— для любого типа, возвращает указатель на zero value.new([]int)вернёт*[]int, указывающий наnil-слайс.
var err error
if true {
x, err := f() // новая err во внутреннем скоупе!
_ = x
}
return err // всегда nilЛечится = вместо := или линтером (go vet -vettool=shadow, govet в golangci-lint).
- Отложенные вызовы выполняются при выходе из функции (не блока) в порядке 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 файлов) — всё закроется только в конце функции. Выносите тело цикла в отдельную функцию.
Переменная цикла теперь создаётся заново на каждой итерации (при 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).
Счётчик внутри const (...), начинается с 0 и растёт на каждой строке.
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
_ // 2 — пропуск
Wednesday // 3
)
const (
_ = iota
KB = 1 << (10 * iota) // 1024
MB // 1048576
)const x = 10 — нетипизированная, имеет «идеальную» точность и приводится к нужному типу при использовании: var f float64 = x работает. const y int = 10 — типизированная, var f float64 = y не скомпилируется.
Всегда по значению (копируется). Но копия слайса/map/канала/указателя/интерфейса содержит указатель на те же данные, поэтому изменения элементов видны снаружи. Для слайса: s[0] = 1 видно вызывающему, а append — нет (если произошла реаллокация или длина вызывающего не изменилась).
Функция, захватывающая переменные из внешней области по ссылке:
func counter() func() int {
n := 0
return func() int { n++; return n }
}n «убегает» в кучу (escape analysis).
Только два уровня видимости: идентификатор с заглавной буквы экспортируется из пакета, со строчной — нет. Плюс каталог internal/ — импортируется только из родительского дерева.
- Инициализируются импортированные пакеты (рекурсивно, каждый один раз).
- Переменные уровня пакета — в порядке зависимостей.
- Все
init()в порядке файлов (как их передалgo build, обычно по алфавиту) и порядке объявления. main(). В одном файле может быть несколькоinit. Злоупотреблять не стоит: неявные побочные эффекты мешают тестам.
outer:
for _, row := range grid {
for _, v := range row {
if v == target { break outer }
}
}Частый вопрос: break внутри select/switch в цикле выходит только из select/switch, а не из цикла.
type A B— новый тип с тем же underlying-типом; методыBне наследуются; нужна явная конвертация.type A = B— алиас, это тот же тип. С Go 1.24 алиасы могут быть параметризованными:type Set[T comparable] = map[T]struct{}.
breakне нужен,fallthrough— явно.switchбез условия = цепочкаif/else.- Type switch:
switch v := x.(type) { case int: ... }.
byte = uint8, rune = int32 (кодовая точка Unicode). Подробно — в следующем разделе.
Отдельный тип + iota + метод String() (генерируется stringer: //go:generate stringer -type=Weekday). Для валидации — метод IsValid().
Игнорирует значение, импорт только ради init (import _ "github.com/lib/pq"), проверка реализации интерфейса на этапе компиляции:
var _ io.Reader = (*MyReader)(nil)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).
Самый «горячий» раздел. Задачи «что выведет код» со слайсами есть почти на каждом Go-собесе.
Структура из трёх слов (24 байта на amd64):
type slice struct {
array unsafe.Pointer // указатель на базовый массив
len int
cap int
}Слайс — «окно» в массив. Несколько слайсов могут делить один массив.
Массив — значение фиксированного размера, размер — часть типа ([3]int ≠ [4]int), копируется целиком при присваивании и передаче. Массивы comparable (можно == и ключом map), слайсы — нет.
- Если
len < cap— пишем в тот же массив, возвращаем слайс сlen+1. - Иначе — выделяем новый массив, копируем. Рост (Go 1.18+): до 256 элементов — ×2, дальше плавно к ×1.25 (
newcap += (newcap + 3*256) / 4), затем округление до size class аллокатора. - Поэтому всегда
s = append(s, x).
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 вынужден реаллоцировать.
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] 3s[0] = 100 виден (общий массив), а новый элемент — нет: len у вызывающего остался 3. Хотя s[:4] покажет 4.
var a []int // nil, len 0
b := []int{} // не nil, len 0Оба безопасны для len, range, append. Разница: a == nil → true; json.Marshal даёт null vs []. В API обычно возвращают nil, в JSON-ответах — пустой, если фронт ждёт массив.
copy(dst, src) копирует min(len(dst), len(src)) элементов. Или slices.Clone(s), или append([]T(nil), s...).
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), если порядок не важенfunc head(b []byte) []byte { return b[:10] } // держит весь мегабайтный массивРешение — скопировать: slices.Clone(b[:10]).
Sort, SortFunc, BinarySearch, Contains, Index, Max, Min, Reverse, Compact, Equal, Insert, Delete, Clone, Grow, Chunk (1.23, итератор), Collect, Sorted, Values (итераторы, 1.23).
- До 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, поэтому при передаче в функцию изменения видны.
Намеренно рандомизирован рантаймом, чтобы никто не полагался на порядок. Для стабильного порядка: slices.Sorted(maps.Keys(m)) (Go 1.23+).
Нет — ошибка компиляции. При росте элементы перемещаются. По той же причине m[k].field = 1 для map[K]Struct не компилируется — нужно v := m[k]; v.field = 1; m[k] = v или хранить указатели.
Чтение — zero value, len = 0, delete — no-op. Запись → panic: assignment to entry in nil map.
Нет. Конкурентная запись (или запись + чтение) → рантайм может упасть с fatal error: concurrent map writes (это не panic, recover не поможет). Решения: sync.Mutex/RWMutex, sync.Map, шардирование.
Любой comparable тип: числа, строки, bool, указатели, каналы, интерфейсы, массивы и структуры из comparable-полей. Нельзя: слайсы, map, функции. Интерфейс с несравнимым динамическим значением скомпилируется, но упадёт в рантайме.
v, ok := m[k]. Для множества — map[T]struct{}.
Нет, число бакетов/групп не уменьшается. clear(m) (Go 1.21) очищает, но тоже не сжимает. Для сжатия — создать новую map.
Оптимизирован для двух сценариев: ключ записывается один раз, а читается много (кэши), и горутины работают с непересекающимися наборами ключей. С Go 1.24 внутри — конкурентное HashTrieMap, стал быстрее. В остальных случаях map + RWMutex проще и типобезопаснее.
Неизменяемая структура {ptr *byte, len int} (16 байт). Байты — обычно UTF-8. Подстрока s[i:j] не копирует данные.
12 — байты. Число символов: utf8.RuneCountInString(s) → 6.
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).
Строки неизменяемы. Нужно b := []byte(s); b[0] = 'a'; s = string(b) (две аллокации) или strings.Builder.
+в цикле —O(n²)из-за копирования.strings.BuilderсGrow(n)— одна аллокация.strings.Joinдля слайса.fmt.Sprintf— удобно, но медленнее (рефлексия, интерфейсы).
Семантически — да. Компилятор оптимизирует частные случаи: m[string(b)] при поиске в map, сравнение string(b) == "x", for range []byte(s). Без копирования вручную — unsafe.String(&b[0], len(b)) / unsafe.Slice(unsafe.StringData(s), len(s)), но только если данные больше не меняются.
Cut (1.18), CutPrefix/CutSuffix (1.20), Lines, SplitSeq, FieldsSeq — итераторы (1.24), CutLast (1.27), bytes.Buffer.Peek (1.26).
Два слова:
iface(непустой интерфейс):{tab *itab, data unsafe.Pointer}, гдеitabхранит тип и таблицу методов.eface(any/interface{}):{_type *_type, data unsafe.Pointer}.
Интерфейс — это пара (динамический тип, значение).
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-указатель.
Тип реализует интерфейс автоматически, если имеет все методы (duck typing на этапе компиляции). Плюсы: нет зависимости от пакета с интерфейсом, легко мокать. Идиома: «Accept interfaces, return structs», интерфейсы объявляются на стороне потребителя и маленькие (io.Reader — 1 метод).
Проверка на этапе компиляции: var _ Storage = (*PgStorage)(nil).
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{} // окПравило: если хоть один метод на указателе (или есть мьютекс внутри) — делайте все на указателе.
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
}Нет, это композиция с продвижением методов. Встроенный тип не знает о внешнем — нет виртуальных вызовов:
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"!Встраивание интерфейса в структуру — приём для частичных моков и декораторов.
== работает, если все поля comparable. Структура со слайсом/map — ошибка компиляции. Интерфейсы с несравнимым значением внутри — panic в рантайме. Для глубокого сравнения в тестах — reflect.DeepEqual или github.com/google/go-cmp.
Занимает 0 байт (все такие значения могут иметь один адрес runtime.zerobase). Применения: множества map[K]struct{}, сигнальные каналы chan struct{}, типы-маркеры с методами.
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 — он выровнен).
Метаданные для рефлексии: 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) — ещё строже и быстрее.
Нет. Только для типов своего пакета. Решение — type MyTime time.Time или обёртка-структура.
Алиас interface{} (Go 1.18). Использовать минимально: теряется типобезопасность, значения упаковываются (boxing → часто аллокация). Сейчас большинство кейсов закрывают дженерики.
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.
При присваивании в интерфейс значение, не помещающееся в указатель (или чей адрес «убегает»), копируется в кучу. Маленькие целые (0–255) и нулевые значения рантайм берёт из статических таблиц. Проверяйте: go build -gcflags=-m.
Встроенный интерфейс type error interface { Error() string }. Ошибки — обычные значения, возвращаются последним результатом.
| Способ | Пример | Проверка |
|---|---|---|
| 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) |
разрывает цепочку — осознанно скрываем детали |
==сравнивает только верхний уровень — после обёртки%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) }errors.Join(err1, err2) (Go 1.20) и fmt.Errorf("%w; %w", a, b) — ошибка с Unwrap() []error. errors.Is проверяет все ветки.
Добавлять контекст что делали, без слов «failed to»/«error»: fmt.Errorf("open config %q: %w", path, err). Итоговое сообщение читается как цепочка: load app: open config "a.yaml": no such file.
Не логировать и возвращать одновременно — ошибка будет залогирована N раз.
Аварийное завершение: раскручивается стек, выполняются defer, затем процесс падает с трейсом. Использовать для ошибок программиста и невозможных состояний (нарушение инвариантов, MustCompile при инициализации). Для ожидаемых ошибок (сеть, ввод) — error.
Возвращает значение паники, только если вызван непосредственно в отложенной функции в той же горутине:
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
}Нет. Паника в любой горутине без recover роняет весь процесс. Поэтому в каждой долгоживущей горутине (воркеры, обработчики) нужен свой defer recover. net/http восстанавливает панику в хендлере сам (и логирует), но горутины, запущенные из хендлера, — нет.
fatal error рантайма: конкурентная запись в map, out of memory, stack overflow (goroutine stack exceeds 1000000000-byte limit), deadlock all goroutines are asleep.
С Go 1.21 panic(nil) превращается в *runtime.PanicNilError, и recover() возвращает не-nil. Раньше recover возвращал nil, и паника была «невидимой».
func main() {
defer fmt.Println("1")
defer func() { recover(); fmt.Println("2") }()
defer fmt.Println("3")
panic("boom")
}
// 3, 2, 1 — программа завершается нормальноДля файлов на запись — да, и это баг: Close может вернуть ошибку сброса буфера. Правильно:
defer func() { err = errors.Join(err, f.Close()) }()Раздел, на котором «валится» больше всего кандидатов уровня Middle. Практика — в разделе задач.
| Горутина | Поток ОС | |
|---|---|---|
| Стек | стартует с 2 КБ, растёт копированием (до 1 ГБ на 64-бит) | 1–8 МБ фиксированно |
| Создание | ~сотни нс, в user space | системный вызов, мкс |
| Переключение | рантайм Go, сохраняются несколько регистров | ядро, полный контекст |
| Планирование | M:N планировщик Go (G-M-P) | планировщик ОС |
| Идентификатор | нет публичного ID (намеренно) | TID |
Конкурентность — структура программы (много независимых задач). Параллелизм — одновременное выполнение на нескольких ядрах. Go даёт конкурентность, а параллелизм зависит от GOMAXPROCS.
runtime.hchan: кольцевой буфер (для буферизированных), sendx/recvx, очереди ожидающих отправителей и получателей (sendq/recvq из sudog), мьютекс. При небуферизированной передаче данные копируются прямо в стек ждущей горутины.
- Небуферизированный: отправка блокируется, пока получатель не заберёт — точка синхронизации (happens-before).
- Буферизированный (
make(chan T, n)): отправка блокируется только при полном буфере. Используется как очередь или семафор.
| Операция | nil-канал |
закрытый канал | открытый канал |
|---|---|---|---|
Чтение <-ch |
блок навсегда | zero value, ok=false сразу |
ждёт данные |
Запись ch <- |
блок навсегда | panic | ждёт место/получателя |
close(ch) |
panic | panic | ок |
len/cap |
0 | остаток в буфере | как есть |
Отправитель, и только когда больше никто не будет писать. Получатель канал не закрывает. При нескольких отправителях — отдельная горутина wg.Wait(); close(ch) или сигнальный канал done. Закрывать канал не обязательно — GC соберёт; закрывают, чтобы сообщить получателям «данных больше нет».
- Ждёт, пока хотя бы один
caseготов; если готово несколько — выбирается случайно (равномерно), чтобы не было голодания. defaultделаетselectнеблокирующим.select {}— блок навсегда.caseсnil-каналом никогда не срабатывает — приём для «отключения» веток.
Горутина, заблокированная навсегда (ждёт канал, который никто не закроет/не прочитает). Растёт память, не освобождаются ресурсы. Поиск:
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падает, если в «пузыре» остались заблокированные горутины.
Семафор на канале, worker pool, errgroup.SetLimit, golang.org/x/sync/semaphore (взвешенный). См. workerpool и parallel.
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.
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 falsefatal error: all goroutines are asleep - deadlock! — только если все горутины заблокированы. Если хоть одна жива (например, HTTP-сервер или тикер), частичный дедлок рантайм не заметит.
func main() {
ch := make(chan int)
ch <- 1 // deadlock: некому читать
}Всё с решениями и тестами — в разделе задач.
Никак принудительно. Только кооперативно: горутина сама проверяет ctx.Done() / закрытый done-канал.
Gosched— уступить процессор (сейчас почти не нужен: с Go 1.14 есть асинхронное вытеснение).Goexit— завершить текущую горутину, выполнивdefer(так работаетt.FailNow).LockOSThread— привязать горутину к потоку (cgo, OpenGL, namespace в Linux).
«Share memory by communicating» — каналы для передачи владения данными и координации (конвейеры, события). Мьютексы — для защиты состояния (кэш, счётчик). Каналы медленнее мьютекса примерно в разы (внутри тот же lock + планирование).
Одновременный доступ к одной переменной из ≥2 горутин, где хотя бы один — запись, без синхронизации. Поведение не определено (рваные значения, в т.ч. у интерфейсов и слайсов — многословных структур).
Поиск: go test -race, go run -race (ThreadSanitizer, замедление ×2–20, память ×5–10). Находит только гонки, которые случились во время прогона.
Data race — низкоуровневый конфликт доступа к памяти. Race condition — логическая ошибка из-за порядка событий (check-then-act), возможна и без data race: if m.Get(k) == nil { m.Set(k, v) } — каждый вызов под мьютексом, а вместе — нет.
Два режима:
- Нормальный: новые горутины конкурируют с разбуженной; сначала спин (на многоядерных), потом парковка через семафор.
- Голодания (starvation): если горутина ждёт > 1 мс, мьютекс передаётся строго по очереди FIFO.
Мьютекс не реентерабельный — повторный
Lockв той же горутине = дедлок. Нельзя копировать после использования (go vet→ copylocks).
Много читателей, мало писателей, и критическая секция чтения не микроскопическая. Иначе overhead RWMutex больше, чем у Mutex. Писатель, ожидающий Lock, блокирует новых читателей (защита от голодания писателей) → рекурсивный RLock может дать дедлок.
var getCfg = sync.OnceValue(func() *Config { return load() }) // Go 1.21
cfg := getCfg()Если функция внутри Once.Do паникует, Once считается выполненным.
Для broadcast-пробуждения многих ожидающих по изменению состояния под мьютексом (Wait всегда в цикле for !cond { c.Wait() }). На практике редко, чаще заменяют закрытием канала.
Кэш временных объектов для снижения нагрузки на GC (буферы, энкодеры). Объекты могут быть удалены в любой GC (с 1.13 — через victim cache переживают один цикл). Не для соединений и не для состояния. Сбрасывайте объект перед Put. Хранить указатели (*bytes.Buffer), а не слайсы — иначе аллокация при упаковке в any.
- Типизированные атомики (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)Атомики быстрее мьютекса для одной переменной, но не делают атомарной группу операций.
Формальные правила 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) // может напечатать ""Две горутины пишут в разные переменные, лежащие в одной кэш-линии (64 байта) → кэш-линия «прыгает» между ядрами. Лечится паддингом _ [56]byte или cpu.CacheLinePad.
Стандартный Mutex — нет (TryLock есть с Go 1.18, но его использование обычно запах дизайна). Альтернатива — семафор на канале: select { case sem <- struct{}{}: case <-ctx.Done(): }.
Пакет 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.
Вопросы уровня Middle+/Senior. Именно тут отличают «пишу на Go» от «понимаю Go».
- G (goroutine) — горутина: стек, PC, статус.
- M (machine) — поток ОС.
- P (processor) — логический процессор: локальная очередь G (до 256), кэш аллокатора
mcache. Число P =GOMAXPROCS. M выполняет G, только владея P. Есть глобальная очередь и слотrunnext(свежесозданная горутина выполнится следующей — локальность).
Если локальная очередь P пуста: проверить глобальную очередь (и её же раз в 61 тик — чтобы не голодала), netpoller, затем украсть половину очереди у случайного P.
M уходит в syscall вместе с G, а P отцепляется (hand off) — sysmon отдаёт его другому M, чтобы остальные горутины продолжали работать. Сетевые операции не блокируют M: они идут через netpoller (epoll/kqueue/IOCP) — горутина паркуется, M свободен.
- Кооперативное: проверка в прологе функций (при росте стека).
- Асинхронное (Go 1.14+):
sysmonзамечает горутину, работающую > 10 мс, и шлёт сигналSIGURGпотоку → горутина прерывается в безопасной точке. Поэтомуfor {}больше не вешает планировщик.
До Go 1.25 = число CPU хоста, из-за чего в поде с limits.cpu: 2 на 64-ядерной ноде было 64 P → троттлинг CFS и рост латентности (раньше лечили go.uber.org/automaxprocs). С Go 1.25 рантайм учитывает cgroup CPU limit и периодически обновляет GOMAXPROCS, если лимит изменился.
Стартовый размер ~2 КБ (с 1.19 — адаптивный, по среднему использованию). При нехватке — выделяется стек ×2 и копируется целиком (contiguous stacks), указатели на стек корректируются. Поэтому указатели на стек нельзя отдавать в C.
Компилятор решает, где разместить переменную: на стеке (дёшево, освобождается при выходе) или в куче (нагрузка на GC). Переменная «убегает», если:
- возвращается указатель на неё;
- сохраняется в глобальную переменную, map, канал, интерфейс (часто);
- захватывается замыканием, живущим дольше функции;
- размер неизвестен на этапе компиляции (
make([]byte, n)) или слишком велик.
go build -gcflags='-m -m' ./...
# ./main.go:10:2: moved to heap: xGo 1.25–1.26 научились размещать на стеке больше слайсов с неконстантным размером.
Основан на TCMalloc: размерные классы (~68 классов до 32 КБ), mcache (на P, без блокировок) → mcentral (на класс) → mheap (страницы, arena по 64 МБ). Мелкие объекты без указателей (< 16 Б) — tiny allocator. В Go 1.27 — size-specialized malloc, до 30% быстрее мелких аллокаций.
Concurrent, non-generational, non-moving, tri-color mark-and-sweep с write barrier.
- Короткая STW-фаза: включить write barrier.
- Конкурентная маркировка: белые (не посещены) → серые (в очереди) → чёрные (обработаны). Горутины, много аллоцирующие, помогают GC (mark assist).
- Короткая STW: завершение маркировки.
- Конкурентная очистка (sweep) — лениво, при аллокациях. Паузы STW обычно < 1 мс.
Новый алгоритм маркировки (эксперимент в Go 1.25, по умолчанию с Go 1.26): сканирует не отдельные объекты, а целые страницы (spans) мелких объектов — лучше локальность памяти и кэшей, меньше промахов. Снижает накладные расходы GC на 10–40% в реальных программах; на новых CPU с векторными инструкциями — ещё ~10%. Отключение: GOEXPERIMENT=nogreenteagc.
GOGC=100(по умолчанию): следующий GC, когда живая куча вырастет на 100%. Больше — реже GC, больше памяти.GOMEMLIMIT(Go 1.19): мягкий лимит памяти рантайма. GC учащается при приближении. Рекомендация для контейнеров:GOMEMLIMIT≈ 80–90% лимита пода.GOGC=off+GOMEMLIMIT— GC только у лимита (осторожно: риск death spiral).
Меньше аллокаций: предвыделение (make(..., 0, n)), sync.Pool, значения вместо указателей, strings.Builder, избегать any/fmt в горячем пути, структуры без указателей (GC их не сканирует). Смотреть go test -bench . -benchmem, pprof -alloc_space, GODEBUG=gctrace=1.
runtime.SetFinalizer — ненадёжен, воскрешает объект, мешает циклам. С Go 1.24 — runtime.AddCleanup(ptr, fn, arg): несколько cleanup на объект, не воскрешает, работает с циклами. Также в 1.24 появился пакет weak (слабые указатели) — для кэшей и интернирования (см. пакет unique, Go 1.23).
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) — всё, что поддерживает< <= > >=.
GC shape stenciling + словари. Код генерируется на каждую «форму» (GC shape): все указательные типы делят одну реализацию, методы вызываются через словарь → косвенный вызов, иногда медленнее интерфейсов и мешает инлайнингу. Для значимых типов (int, float64) — отдельная специализированная копия, быстро.
Вывод для собеса: дженерики — для структур данных и алгоритмов (контейнеры, slices, maps), а не замена интерфейсам в бизнес-логике.
- Интерфейс — когда важно поведение (методы), а конкретный тип не важен.
- Дженерик — когда одна и та же логика для разных типов и важно сохранить тип (без
anyи type assertion), напримерMax[T],Cache[K, V].
- Методы с собственными параметрами типа — можно с Go 1.27 (см. ниже). Но в интерфейсах методы с параметрами типа по-прежнему запрещены.
- Специализация (разная реализация для конкретного
T) — нет, только type switch поany(v). - Параметры типа в type switch/
caseнапрямую, variadic type parameters — нет.
Методы теперь могут объявлять свои параметры типа:
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. Ограничение: такие методы не участвуют в реализации интерфейсов.
type Adder[A Adder[A]] interface { Add(A) A }
func SumAll[A Adder[A]](xs ...A) A { /* ... */ }Раньше тип не мог ссылаться на себя в своём списке параметров типа.
type Set[T comparable] = map[T]struct{}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 в цикле потребителя.
Компилятор выводит параметры по аргументам: Map(xs, strconv.Itoa). С Go 1.21 — и по типу присваивания/возвращаемому значению; в Go 1.27 инференс работает во всех контекстах, где дженерик-функция присваивается переменной или конвертируется в тип функции.
Решения: lrucache, intersect, pipeline.Map.
Передача по цепочке вызовов (и между горутинами) сигнала отмены, дедлайна и request-scoped значений (trace ID, user ID). Контекст — дерево: отмена родителя отменяет всех потомков, но не наоборот.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error // nil, Canceled или DeadlineExceeded
Value(key any) any
}| Функция | Версия | Назначение |
|---|---|---|
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 |
Иначе дочерний контекст (и его таймер) живёт до отмены родителя → утечка. go vet предупреждает (lostcancel). Идиома: ctx, cancel := context.WithTimeout(...); defer cancel().
- Первый параметр функции:
func Do(ctx context.Context, ...). - Не хранить в структурах (исключение — структуры, представляющие одну операцию).
- Не передавать
nil— используйтеcontext.TODO(). - Ключи
WithValue— свой неэкспортируемый тип, чтобы не было коллизий:
type ctxKey struct{}
ctx = context.WithValue(ctx, ctxKey{}, userID)- В
Value— только request-scoped данные, не параметры функций и не зависимости (логгер/БД — спорно; лучше явно).
Каждый WithValue — новый узел связного списка; поиск идёт от листа к корню, O(глубина). Не кладите туда десятки значений.
http.NewRequestWithContext(ctx, ...), db.QueryContext(ctx, ...), grpc — принимают ctx и прерывают операцию. В net/http сервере r.Context() отменяется при разрыве клиентского соединения или завершении ServeHTTP.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
child, cancelChild := context.WithTimeout(ctx, time.Hour)
defer cancelChild()
cancel()
fmt.Println(child.Err()) // context canceled — дедлайн потомка не может быть позже родительского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: }.
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() больше не нужен.
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).
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.
Интерфейс на стороне потребителя + ручной фейк или генерация: go.uber.org/mock (mockgen, форк gomock), mockery, minimock (популярен в РФ). Для HTTP — httptest.NewServer, httptest.NewRecorder; в Go 1.27 — httptest.NewTestServer (in-memory). Для БД — testcontainers-go или sqlmock.
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.
Детерминированное тестирование кода с time и горутинами без реальных Sleep. См. раздел про sync.
go test -coverprofile=c.out ./... && go tool cover -html=c.out. Для интеграционных тестов — go build -cover + GOCOVERDIR.
| Профиль | Что показывает |
|---|---|
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.
Трассировка показывает работу планировщика, GC, блокировки по времени. Go 1.25: runtime/trace.FlightRecorder — держит последние секунды трейса в кольцевом буфере и сохраняет их, когда случилась аномалия (например, медленный запрос).
Положите CPU-профиль продакшена как default.pgo в пакет main — go build использует его для инлайнинга и девиртуализации. Типичный выигрыш — 2–14%.
go vet (обязателен), staticcheck, golangci-lint (агрегатор), govulncheck (уязвимости зависимостей), go fix — с Go 1.26 применяет модернайзеры (переводит код на min/max, slices, range int, wg.Go и т.д.).
ListenAndServe → Accept в цикле → горутина на каждое соединение → чтение запроса → Handler.ServeHTTP(w, r). Роутинг — http.ServeMux.
Методы и 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)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.
- Сервер:
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 к одному хосту.
См. задачу gracefulshutdown.
log/slog (Go 1.21) — структурированный логгер в стандартной библиотеке; slog.NewMultiHandler (Go 1.26). Раньше — zap, zerolog.
encoding/json — через рефлексию, медленный. Опции тегов: omitempty, omitzero (1.24), string, -. Go 1.27: encoding/json/v2 и encoding/json/jsontext — быстрее, строже (отклоняет невалидный UTF-8, дубликаты ключей), поддерживает стриминг. Альтернативы: easyjson, sonic, goccy/go-json.
| gRPC | REST/JSON | |
|---|---|---|
| Транспорт | HTTP/2, мультиплексирование | HTTP/1.1 или 2 |
| Формат | Protobuf (бинарный, схема) | JSON (текст) |
| Стриминг | unary, server, client, bidi | SSE/WebSocket отдельно |
| Контракт | .proto + кодогенерация |
OpenAPI (опционально) |
| Браузер | нужен gRPC-Web / Connect | нативно |
- Interceptors (unary/stream) — аналог middleware.
- Дедлайны передаются в метаданных и превращаются в
ctxна сервере. - Коды ошибок
status.Error(codes.NotFound, ...). - Балансировка: HTTP/2 держит одно долгое соединение → L4-балансировщик распределяет плохо; нужен client-side LB (
round_robin) или L7 (Envoy). - Обратная совместимость protobuf: нельзя менять номера полей, удалённые —
reserved.
sql.DB — пул, потокобезопасен, создаётся один раз. Настройки: SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime, SetConnMaxIdleTime. Для Postgres в Go-мире стандарт — jackc/pgx/v5 (+ pgxpool).
Не закрыли rows → соединение не вернётся в пул: всегда defer rows.Close() и проверка rows.Err(). QueryRow закрывается сам после Scan.
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 |
B-tree (по умолчанию, =, диапазоны, сортировка), Hash, GIN (jsonb, массивы, полнотекст), GiST/BRIN. Составной индекс и правило левого префикса. Покрывающий индекс (INCLUDE). EXPLAIN (ANALYZE, BUFFERS). Почему индекс не используется: функция над колонкой, низкая селективность, LIKE '%x', несовпадение типов.
- N+1 — запрос в цикле →
WHERE id = ANY($1)или JOIN. - Инъекции — только плейсхолдеры (
$1), никакогоfmt.Sprintfв SQL. - В Go чаще
sqlc(генерация из SQL),squirrel/goqu, реже GORM/ent.
goose, golang-migrate, atlas. Правило zero-downtime: расширяем → деплоим код → сужаем (expand/contract), CREATE INDEX CONCURRENTLY.
Топик → партиции → упорядоченность только внутри партиции (ключ сообщения определяет партицию). Consumer group: одна партиция — один консьюмер группы. Offset commit после обработки → at-least-once → идемпотентные обработчики. Exactly-once — транзакции Kafka или дедупликация на стороне получателя. Клиенты: segmentio/kafka-go, twmb/franz-go, IBM/sarama, confluent-kafka-go.
Как атомарно записать в БД и отправить событие? Пишем событие в таблицу outbox в той же транзакции, отдельный процесс читает и публикует в Kafka (или CDC через Debezium).
Типы данных, TTL, стратегии кэширования (cache-aside, write-through, write-behind), инвалидация, cache stampede (singleflight), распределённый лок (SET NX PX + fencing token; Redlock и его критика), Lua-скрипты для атомарности.
Официальный гайд: 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 — это прежде всего направление зависимостей внутрь и интерфейсы на стороне потребителя.
- S — маленькие пакеты с одной ответственностью.
- O — расширение через интерфейсы и композицию.
- L — любая реализация интерфейса взаимозаменяема.
- I — маленькие интерфейсы (
io.Reader,io.Writer) →io.ReadWriterкомпозицией. - D — сервис зависит от интерфейса
UserRepo, объявленного в пакете сервиса, а не от*PostgresRepo.
В Go обычно ручная сборка в main (конструкторы NewService(repo, logger)). Для больших проектов — google/wire (кодогенерация) или uber-go/fx (рантайм).
| Паттерн | В 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 | экспоненциальная задержка + случайность |
- Идемпотентность (idempotency key), ретраи только для идемпотентных операций.
- Таймауты и дедлайны по всей цепочке, circuit breaker, bulkhead.
- Распределённые транзакции: Saga (оркестрация/хореография) вместо 2PC; Outbox.
- Observability: логи (
slog), метрики (Prometheus: RED/USE), трейсы (OpenTelemetry). - Health checks: liveness vs readiness.
- Конфигурация через env (12-factor).
Counter (только растёт), Gauge (вверх-вниз), Histogram (бакеты, агрегируется между инстансами → p99 через histogram_quantile), Summary (квантили на клиенте, не агрегируется). Не кладите user ID в лейблы — взрыв кардинальности.
Вопросы для Senior/Staff и команд, где Go — основной язык (Яндекс, Ozon, Авито, VK, Wildberries, Kaspersky, биржи и финтех). Здесь не проверяют «знаете ли вы Go», здесь проверяют, понимаете ли вы, что происходит под капотом и можете ли объяснить поведение программы в проде.
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 очередь.
Компилятор оптимизирует частные случаи: select {} — вечная блокировка; один case — обычная операция с каналом; один case + default — неблокирующие selectnbsend/selectnbrecv. Общий случай — runtime.selectgo:
pollorder— случайная перестановка кейсов (поэтому выбор среди готовых случаен и нет голодания).lockorder— кейсы, отсортированные по адресу канала: все каналы блокируются в едином порядке, чтобы дваselectне устроили deadlock друг другу.- Проход по
pollorder: если что-то готово — выполняем и выходим. Естьdefault— выходим через него. - Иначе горутина ставит свой
sudogв очереди всех каналов и паркуется. Когда один канал её будит, она снимаетsudogиз остальных очередей.
Вывод для собеса: select на N каналах — это O(N) блокировок на каждый вызов; большой select в горячем цикле дорог.
- Таймеры хранятся в 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.
Системный монитор — отдельный поток без P (не участвует в планировании Go-кода), просыпается каждые 20 мкс…10 мс (засыпает дольше, если всё спокойно):
- отбирает P у потоков, застрявших в syscall/cgo, и отдаёт другому M;
- вытесняет горутины, работающие дольше 10 мс (сигнал
SIGURG, асинхронное вытеснение); - опрашивает netpoller, если его давно никто не проверял;
- форсирует GC, если его не было 2 минуты;
- будит scavenger, возвращающий неиспользуемую память ОС.
Во время конкурентной маркировки программа (mutator) меняет указатели, и GC может «потерять» живой объект: чёрный объект начинает ссылаться на белый, а единственный серый путь к белому удаляется. Write barrier перехватывает запись указателя и красит объекты в серый.
- До Go 1.8 — барьер Дейкстры (insertion), но стеки горутин им не покрывались → в конце маркировки нужен был STW с повторным сканированием всех стеков (десятки мс на больших программах).
- С Go 1.8 — гибридный барьер (Yuasa deletion + Dijkstra insertion): красится и старое, и новое значение указателя. Стек каждой горутины сканируется один раз и больше не пересканируется → STW-паузы стали субмиллисекундными.
- Барьер включён только во время маркировки; вне её запись указателя — просто проверка флага.
- Map = directory указателей на таблицы (до 1024 слотов каждая); таблица = массив групп по 8 слотов + 8-байтовое контрольное слово (по байту на слот: пусто / удалён / 7 младших бит хэша —
H2). - Хэш делится на
H1(старшие 57 бит — выбор группы и пробирование) иH2(7 бит — в контрольном слове). - Поиск: по
H1находим группу, одним сравнением 8 байт контрольного слова (SIMD или SWAR-трюк на обычных регистрах) находим слоты-кандидаты с совпадающимH2, и только их ключи сравниваем полностью. Дальше — квадратичное пробирование по группам. - Рост: растёт только переполненная таблица (расширяемое хэширование): таблица делится на две, directory удваивается при необходимости. Нет больших «стоп-мир» рехэшей и нет overflow-бакетов.
- Маленькие map (≤ 8 элементов) — одна группа без directory и таблиц.
- Итерация остаётся рандомизированной и корректной при вставках во время
range(итератор держит ссылку на старую таблицу).
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(), иначе корутина и её ресурсы останутся жить.
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).
- Бюджет — 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 — запретить (нужно в бенчмарках, чтобы компилятор не выкинул код).
Каждое 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.
Кладёте 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.
| Директива | Что делает |
|---|---|
//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) |
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, главное:
*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».
// 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) и конкатенации, результат которой сразу сравнивается.
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.
- Каждый вызов 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-резолвер).
- Горутины, заблокированные навсегда (самая частая) — держат стек и всё, на что ссылаются.
- Подслайс/подстрока большого буфера:
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.
- Предвыделять:
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.
ReadMemStats делает stop-the-world — вызывать его раз в секунду в проде плохо. runtime/metrics (Go 1.16) читает метрики без STW и даёт больше: гистограммы пауз GC (/gc/pauses:seconds), задержку планировщика (/sched/latencies:seconds — сколько горутины ждут в очереди P, лучший индикатор нехватки CPU и троттлинга), число горутин, лимиты. prometheus/client_golang с новыми версиями использует именно его.
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.
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— удалять запись из кэша, когда объект умер.
Основа — 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 выигрывает только при очень высокой конкуренции и маленькой критической секции — и его сложно доказать корректным.
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, таблиц маршрутизации, сертификатов.
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.
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").
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.
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?». Этот ответ показывает, что вы следите за языком. Источник — официальные release notes.
- 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.
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.
- 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.
- 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.
- Итераторы (range-over-func), пакет
iter, функции-итераторы вslicesиmaps. - Пакет
unique(интернирование). - Таймеры: собираются GC без
Stop, каналы таймеров синхронные. - Телеметрия тулчейна (opt-in).
- Новая семантика переменной цикла — своя на каждой итерации.
for i := range 10.- Роутинг с методами и
{wildcard}вhttp.ServeMux. math/rand/v2.
- Требования (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.
Дешёвые горутины для IO-bound нагрузки, netpoller, предсказуемые паузы GC, статический бинарник для контейнеров. На Go написаны Kubernetes, Docker, etcd, Prometheus, CockroachDB, VictoriaMetrics, Consul, Terraform.
Senior-интервью в 2026 почти всегда включает блок «как ваш сервис ведёт себя, когда всё ломается». Ответ «добавим ретраи» без деталей — красный флаг.
- Повторять только идемпотентные операции и только временные ошибки (таймаут, 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.
Ретраи помогают пережить кратковременный сбой; circuit breaker защищает от длительного: после N ошибок подряд (или % ошибок в окне) переходит в Open и сразу отвечает ошибкой, не нагружая упавший сервис; через таймаут — Half-Open, пропускает пробный запрос; успех → Closed. Плюс fallback (кэш, дефолт, деградация фичи). Библиотеки: sony/gobreaker. Реализация — Circuit Breaker.
Клиент генерирует Idempotency-Key (UUID) на операцию и шлёт его при каждом ретрае. Сервер в транзакции: INSERT ... ON CONFLICT (key) DO NOTHING в таблицу ключей → если ключ уже есть, возвращает сохранённый ответ, а не выполняет операцию повторно. Хранить ключи с TTL. Для консьюмеров Kafka — дедупликация по ID сообщения (inbox pattern). Exactly-once в распределённой системе = at-least-once доставка + идемпотентная обработка.
- 2PC: координатор просит всех «подготовиться», затем «закоммитить». Строгая атомарность, но блокирующий: упал координатор между фазами — участники держат блокировки. Плохо масштабируется, не поддерживается большинством брокеров/HTTP-API.
- Сага: цепочка локальных транзакций, для каждой — компенсирующее действие (отменить бронь, вернуть деньги). Нет изоляции (промежуточные состояния видны), нужна идемпотентность шагов и компенсаций.
- Оркестрация — центральный координатор (Temporal, свой state machine в БД): проще отлаживать.
- Хореография — сервисы реагируют на события друг друга: меньше связности, сложнее понять общий поток.
- Надёжная публикация событий шага — через outbox.
SET key token NX PX 30000 + снятие Lua-скриптом, сверяющим token. Проблемы: процесс уснул (GC-пауза, своп) дольше TTL → блокировка истекла, её взял другой, а первый «проснулся» и пишет. Redlock эту проблему не решает. Решение — fencing token: хранилище выдаёт монотонно растущий номер при захвате, а ресурс отклоняет запись с номером меньше уже виденного. Если нужна корректность, а не только эффективность, — etcd/ZooKeeper (lease + revision) или блокировка в самой БД (SELECT ... FOR UPDATE, advisory locks в Postgres).
При hash(key) % N добавление узла перемещает почти все ключи. В consistent hashing узлы и ключи ставятся на кольцо; ключ принадлежит первому узлу по часовой стрелке → при изменении числа узлов перемещается ~1/N ключей. Виртуальные узлы (100–200 на физический) выравнивают нагрузку. Альтернативы: rendezvous hashing (HRW), jump consistent hash (без памяти, но только добавление в конец). Реализация — Consistent hashing.
- Backpressure — медленный потребитель замедляет производителя: ограниченные каналы/очереди, семафоры, HTTP 429 +
Retry-After. Неограниченная очередь = отложенный OOM и огромная латентность. - Load shedding — при перегрузке сразу отказывать части запросов (по приоритету, по времени ожидания в очереди — запрос, который ждёт дольше своего дедлайна, бессмысленно обрабатывать).
- Bulkhead — отдельные пулы (соединений, горутин) на разные зависимости, чтобы медленная зависимость не выела все ресурсы.
- Adaptive concurrency limits (алгоритмы Vegas/Gradient, как в Netflix concurrency-limits) — лимит подстраивается по латентности.
Приём из статьи Google «The Tail at Scale»: если ответ не пришёл за время ~p95, отправить дубликат запроса в другую реплику и взять первый ответ, отменив второй через context. Резко снижает p99 ценой ~5% дополнительной нагрузки. Только для идемпотентных чтений. Очень похоже на задачу «первый успешный ответ из N реплик».
В gRPC дедлайн из context автоматически передаётся в заголовке grpc-timeout и восстанавливается на сервере — вся цепочка вызовов укладывается в исходный бюджет. В HTTP это нужно делать вручную (свой заголовок, например X-Request-Deadline) или через OpenTelemetry baggage. На сервере — сразу проверять остаток времени и не начинать работу, которая заведомо не успеет.
Kubernetes: client-go/tools/leaderelection (Lease-объект). etcd: concurrency.NewElection на сессии с lease — при потере соединения lease истекает и лидерство переходит. Postgres: pg_try_advisory_lock. Всегда учитывайте, что бывший лидер может ещё не знать, что он больше не лидер (снова fencing), и что задача может выполниться дважды — делайте её идемпотентной.
- Скрининг с рекрутером (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 — найти баги в чужом коде (гонки, утечки, необработанные ошибки).
- Финальное интервью с командой / руководителем — поведенческие вопросы.
- Уточнение требований и граничных случаев до кода.
- Рассуждение вслух: сначала наивное решение и его сложность, потом оптимизация.
- Чистый, идиоматичный Go: обработка ошибок, имена, отсутствие гонок.
- Самопроверка: прогон примеров, граничные случаи (пустой ввод, один элемент, дубликаты, переполнение).
- Гонка при записи в map /
appendиз горутин. - Горутина без выхода (утечка), незакрытые
resp.Body/rows. deferв цикле.- Захват переменной цикла (для кода с
go< 1.22 вgo.mod!). - Типизированный nil в интерфейсе ошибки.
- Копирование структуры с
sync.Mutex. http.Clientбез таймаута,context.Background()вместо входящего ctx.- SQL через
fmt.Sprintf(инъекция).
- Самая сложная задача / инцидент в проде и как вы его разбирали.
- Конфликт в команде или с продактом.
- Техническое решение, о котором вы пожалели.
- Как вы принимаете решения при неполной информации.
| Срок | Что делать |
|---|---|
| 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.
Формат, который есть почти на каждом Go-собеседовании в Яндексе, Ozon, Авито, Wildberries, Т-Банке и Сбере. Сначала ответьте сами, потом раскройте ответ. Все ответы проверены на Go 1.24+ с
go 1.22+вgo.mod.
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].
s := make([]int, 0, 1)
s1 := append(s, 1)
s2 := append(s, 2)
fmt.Println(s1, s2)Ответ
[2] [2] — оба append пишут в один и тот же элемент общего массива (места хватает, реаллокации нет).
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.
s := []int{1, 2, 3}
for i := range s {
s = append(s, i)
}
fmt.Println(s)Ответ
[1 2 3 0 1 2] — выражение 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++ }.
i := 1
defer fmt.Println(i)
i = 2Ответ
1 — аргументы вычисляются в момент defer. С замыканием defer func(){ fmt.Println(i) }() было бы 2.
func f() (n int) {
defer func() { n *= 2 }()
return 3
}
fmt.Println(f())Ответ
6 — return 3 присваивает n = 3, затем выполняется defer.
for i := range 3 {
defer fmt.Print(i)
}Ответ
210 — LIFO.
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.
var p *int
var e any = p
fmt.Println(e == nil, p == nil)Ответ
false true
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.
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.
func helper() { fmt.Println(recover()) }
func main() {
defer func() { helper() }()
panic("boom")
}Ответ
Печатает <nil>, программа падает с panic: boom. recover работает, только если вызван непосредственно отложенной функцией. defer helper() — сработал бы.
func main() {
defer func() { recover() }()
go func() { panic("boom") }()
time.Sleep(time.Second)
}Ответ
Программа падает. recover в main не ловит панику из другой горутины.
func main() {
go fmt.Println("hello")
}Ответ
Скорее всего ничего — main завершается, процесс умирает вместе со всеми горутинами.
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.
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).
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.
var ch chan int
ch <- 1Ответ
fatal error: all goroutines are asleep - deadlock! — запись в nil-канал блокирует навсегда (единственная горутина).
var x uint8 = 255
x++
fmt.Println(x, -7/2, -7%2)Ответ
0 -3 -1 — беззнаковое переполнение по модулю; деление усекается к нулю, знак остатка = знак делимого.
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 отдаёт байтовые индексы начала рун.
a := [3]int{1, 2, 3}
b := a
b[0] = 100
fmt.Println(a, b)Ответ
[1 2 3] [100 2 3]
dst := make([]int, 2)
n := copy(dst, []int{7, 8, 9})
fmt.Println(n, dst)Ответ
2 [7 8] — копируется min(len(dst), len(src)).
type U struct {
Name string
age int
}
b, _ := json.Marshal(U{"go", 5})
fmt.Println(string(b))Ответ
{"Name":"go"} — рефлексия не видит неэкспортируемые поля.
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.
Задачи, на которых «сыпятся» даже опытные разработчики: NaN-ключи, итераторы, таймеры Go 1.23, дженерики и
comparable, выравнивание. Все ответы проверены на Go 1.24 (go 1.24вgo.mod).
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).
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.
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.
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» не требуется), поэтому код компилируется, но сравнение интерфейсов с несравнимыми динамическими типами паникует в рантайме.
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 — значение уже лежало в буфере канала.
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.
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.
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.
for i := range 3 {
defer func() { fmt.Print(i) }()
}Ответ
210. С Go 1.22 у каждой итерации своя i (замыкания видят 0, 1, 2), а defer выполняются в обратном порядке. До 1.22 было бы 333.
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 вынужден выделить новый массив, исходный не затронут.
s := []int{1, 2, 3, 4, 5}
copy(s[1:], s)
fmt.Println(s)Ответ
[1 1 2 3 4]. copy корректно работает с перекрывающимися областями (как memmove), поэтому это стандартный способ вставить элемент со сдвигом.
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.
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.
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) в такой ситуации повторяют ту же панику при каждом вызове — это безопаснее.
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-пакет: условие, разбор, ловушки, follow-up вопросы и решение на Go. Все решения проверены go test -race.
Как тренироваться: прочитайте условие, закройте решение, напишите своё за 20–30 минут, сравните с решением.
| # | Задача | Паттерн | Уровень |
|---|---|---|---|
| 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 |
| # | Задача | Что проверяют | Уровень |
|---|---|---|---|
| 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 |
| # | Задача | Что проверяют | Уровень |
|---|---|---|---|
| 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 |
Уровень: 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. - Забывают про пустой массив и отсутствие ответа.
- 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 по меньшему.
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
}Уровень: 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()
}Уровень: 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
}Уровень: 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
}Уровень: 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
}Уровень: Middle · Где спрашивают: Яндекс, Т-Банк, Google, Avito
Дан список отрезков [start, end]. Слейте все пересекающиеся.
Сортируем по start, идём по списку и расширяем последний отрезок результата, пока новый начинается не позже его конца.
O(n log n) время, O(n) память.
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
}Уровень: 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 |
container/heapтребует реализоватьheap.Interface(5 методов). Push/Pop — с указательным ресивером.- Порядок обхода
mapслучайный → без tie-break результат недетерминирован, тесты «мигают».
- Поток бесконечный, памяти мало → 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
}Уровень: 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()
}Уровень: 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
}Уровень: 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.
- Сохранить порядок? → слияние отсортированных потоков через 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
}Уровень: 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
}Уровень: 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...)...)
}Уровень: 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) }) }Уровень: 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)
}
*/Уровень: 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
}Уровень: 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
}Уровень: 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)
}
}Уровень: 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
}Уровень: Senior · Где спрашивают: Ozon, Авито, Т-Банк, VK — как часть разговора про отказоустойчивость
Реализуйте circuit breaker: после N ошибок подряд он «размыкается» и сразу возвращает ErrOpen; через timeout пропускает ровно один пробный запрос; успех — замыкается, ошибка — снова размыкается.
- Состояния Closed → Open → Half-Open — нарисуйте автомат перед кодом, это оценивают.
- Сам вызов
fn()— вне мьютекса, иначе breaker сериализует весь трафик. - В Half-Open пускаем один пробный запрос (флаг
probing), остальные получаютErrOpen— иначе после восстановления на лежащий сервис хлынет весь накопленный поток. - Часы инъектируются (
now func() time.Time) → тесты безtime.Sleep.
- Порог по доле ошибок в скользящем окне вместо «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)
}Уровень: 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).
- Коллизии хэшей виртуальных узлов? (перезапишут
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)
}Уровень: 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 — соседние мьютексы не должны жить в одной кэш-линии.
- Когда
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")
}
}Уровень: 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 невозможна.
- Будет ли это быстрее
[]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())
}
}Уровень: 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).
- 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)
}
}Отмечайте [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)
- 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
- Внутренности
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
-
net/http: ServeMux с 1.22, middleware, таймауты, graceful shutdown - gRPC vs REST, interceptors, дедлайны
-
database/sql/pgx: пул, транзакции, уровни изоляции, индексы - Kafka: партиции, consumer groups, семантики доставки, outbox
- Redis: кэширование, инвалидация, распределённые локи
- Observability: slog, Prometheus, OpenTelemetry
- Алгоритм ответа, оценка нагрузки
- 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.