Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Что такое горутина и чем она отличается от потока ОС?
Горутина — это легковесный поток в рантайме Go, которым управляет планировщик Go, а не операционная система. Отличия трёх уровней. Размер: горутина стартует со стеком около 2 КБ и растёт по необходимости (до гигабайта), поток ОС резервирует мегабайты. Цена создания: запуск горутины — сотни наносекунд и несколько сотен байт служебных данных, создание потока — системный вызов на микросекунды и килобайты. Планирование: тысячи горутин мультиплексируются на небольшое число потоков ОС (модель M:N): планировщик Go сам раздаёт горутины на логические процессоры (GOMAXPROCS), вытесняет долгие вычисления по точкам безопасного останова и умеет красть работу у занятых потоков (work stealing).
go func() {
fmt.Println("горутина запущена одним словом go")
}()2. Чем буферизованный канал отличается от небуферизованного?
Небуферизованный канал — это синхронизация «рукопожатием»: отправка блокируется, пока кто-то не примет, приём блокируется, пока кто-то не отправит; обе стороны встречаются в моменте. Буферизованный канал — очередь фиксированной ёмкости: отправка проходит сразу, если в буфере есть место, и блокируется только при полном буфере; приём блокируется только при пустом. На собеседовании ждут вывода: буфер не «ускоряет» канал сам по себе — он лишь развязывает по времени отправителя и получателя. Выбор ёмкости — это решение о допустимой задержке и пике памяти, а не оптимизация по умолчанию.
ch := make(chan int) // небуферизованный: send ждёт receive ch2 := make(chan int, 3) // буфер на 3: первые 3 send проходят сразу
3. Что произойдёт при отправке в закрытый канал и при чтении из закрытого?
Правило двух сторон. Отправка в закрытый канал — паника «send on closed channel»: закрытие говорит «новых значений не будет», поэтому писать после него нельзя. Чтение из закрытого канала паники не вызывает: приём немедленно возвращает нулевое значение типа и false во второй переменной (comma-ok). Закрывает канал всегда отправитель; если отправителей несколько, закрытие согласовывают через sync.Once или отдельный канал сигнала. Закрывать канал со стороны получателя — классическая ошибка: отправитель на не закрытый вовремя канал зависнет, а на закрытый — запаникует.
ch := make(chan int, 2) ch <- 1 close(ch) v, ok := <-ch // 1, true v, ok = <-ch // 0, false — канал закрыт и пуст
4. Как работает select? Что делает default и что будет при нескольких готовых case?
select ждёт готовности нескольких операций с каналами одновременно: срабатывает тот case, чья операция может выполниться. Если готовы несколько — рантайм выбирает случайный, равновероятный; это защищает от голодания одного из каналов. Пустой select{} блокирует горутину навсегда. Ветвь default срабатывает, когда ни одна операция не готова — это превращает select в «попробовать без ожидания» (non-blocking) или в опрос с таймаутом через time.After. Таймаут в select — идиома, которую спрашивают почти всегда: ожидание значения и time.After(d) в одном select, таймаут-ветвь спасает от вечного зависания.
select {
case v := <-ch:
fmt.Println("получили", v)
case <-time.After(2 * time.Second):
fmt.Println("таймаут")
default:
fmt.Println("нечего читать — идём дальше")
}5. Горутина пишет в канал, а читать её никто не будет. Что случится?
Утечка горутины — самый популярный практический вопрос. Отправка в небуферизованный (или полный) канал без читателя блокирует горутину навсегда: она не будет собрана сборщиком мусора, пока жив канал, и держит свой стек. Классические сценарии: горутина ждёт значение из канала, который уже никто не закроет; worker забыл обработать отмену контекста; время.After в цикле при пропуске чтения. Лечение — проектировать выход: контекст с отменой, select с ctx.Done() и таймаутами, отдельный канал-сигнал завершения. Диагностика: pprof goroutine profile и инструменты вроде goleak в тестах.
func worker(ctx context.Context, ch <-chan int) {
for {
select {
case v, ok := <-ch:
if !ok {
return // канал закрыт
}
process(v)
case <-ctx.Done():
return // отмена — горутина не утекает
}
}
}6. Что такое состояние гонки (data race) и как его найти?
Состояние гонки — одновременный доступ двух горутин к одной переменной, когда хотя бы один доступ — запись, и никакой синхронизации нет. Гонка — это не «неудачное чередование», а неопределённое поведение: чтение может увидеть частично записанное значение. Go ловит это детектором: тесты или бинарник с флагом -race инструментируют доступ к памяти и печатают трассу при обнаружении. Детектор находит только те гонки, которые реально произошли в прогоне, поэтому важен параллельный тест с t.Parallel и нагрузкой. Защита — по ситуации: каналы («не общайтесь памятью, делитесь памятью через общение»), sync.Mutex для критических секций, sync/atomic для счётчиков и флагов.
go test -race ./...
7. Для чего нужны sync.WaitGroup и errgroup?
sync.WaitGroup ждёт завершения набора горутин: Add(n) перед запуском, Done() через defer внутри каждой, Wait() блокирует до нуля. Подводный камень — Add после Wait: счётчик надо наращивать до старта ждущего. errgroup из golang.org/x/sync — надстройка для задач «запусти пачку и собери первую ошибку»: Go возвращает ошибку, Wait дожидается всех и отдаёт первую, WithContext привязывает группу к контексту и отменяет всех при первой ошибке. На собеседовании полезно назвать разницу: WaitGroup — только «дождись», errgroup — «дождись и обработай ошибку с отменой».
g, ctx := errgroup.WithContext(ctx)
for _, u := range urls {
u := u
g.Go(func() error { return fetch(ctx, u) })
}
if err := g.Wait(); err != nil {
log.Fatal(err) // первая ошибка; остальные отменены через ctx
}8. Как ограничить число одновременно работающих горутин?
Три идиомы. Семафор на канале: make(chan struct{}, N), горутина перед работой пишет в него (захват слота) и читает по завершении — больше N одновременно не войдут. Пул воркеров: N горутин читают задачи из общего канала до его закрытия — самый частый паттерн в продакшене. errgroup.SetLimit(N) — готовое ограничение поверх errgroup: Go блокируется, пока не освободится слот. В ответе стоит сказать и про ошибку «запустить десять тысяч горутин разом»: память стека мелкая, но работа обычно упирается в системы ниже по потоку — базу, сеть, — и ограничение параллелизма защищает их.
sem := make(chan struct{}, 5)
for _, job := range jobs {
job := job
go func() {
sem <- struct{}{} // захват слота
defer func() { <-sem }() // освобождение
handle(job)
}()
}9. Почему main может завершиться раньше горутин?
Завершение main завершает всю программу, не дожидаясь остальных горутин: спавн горутины не создаёт связи «родитель ждёт ребёнка». Если результат горутин нужен — ждите явно: sync.WaitGroup, чтение из каналов-результатов, errgroup.Wait. Если результат не нужен, но работу важно довести — это отдельный вопрос жизненного цикла: graceful shutdown по сигналу, контекст с таймаутом, отложенные задачи в очередь (asynq). Ответ «поставлю time.Sleep» — красный флаг: sleep фиксирует гонку со временем вместо синхронизации.
10. Что выведет программа: value := <-ch из nil-канала?
Операции с nil-каналом блокируются навсегда: отправка, приём и close на нём вечны (close — паника). select с nil-каналом просто игнорирует этот case — на этом построен приём «отключить ветку»: присвоить case-каналу nil после того, как он больше не нужен, и select перестанет его опрашивать. Нулевой канал получается из make без инициализации, из map или структуры, где поле-канал не заполнили. На собеседовании это вопрос на знание трёх состояний канала: nil — блокируется навсегда; открытый — работает; закрытый — отдаёт нулевое значение и ok=false, отправка паникует.
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Asynq: полный курс по фоновым задачам в Go — Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.