Собеседование Golang
Полный гайд: какие темы решают исход, разбор частых вопросов и шесть задач с решением — для junior, middle и senior.
// обновлено 07.2026 · темы, вопросы и практика
Собеседование по Go отличается от собеседований по другим языкам одним акцентом: синтаксис здесь маленький, а спрашивают глубоко. Проверяют не «знаете ли вы язык», а понимаете ли конкурентную модель, устройство слайсов и интерфейсов и десяток идиоматичных подводных камней, на которых спотыкаются даже опытные.
Этот гайд — карта того, что действительно спрашивают, разбор ключевых тем и шесть задач с решением, которые реально дают на лайвкодинге. Подойдёт на позицию Go-разработчика любого грейда. А готовиться к самому интервью удобно с нейросетью для собеседований ENIGMA — тренажёр и разбор ответов.
Как устроено собеседование по Go
Скелет процесса стандартный: скрининг, техническая секция, лайвкодинг, а для middle+ и senior — system design. Но акценты у Go свои.
- Конкурентность в центре. Горутины, каналы, синхронизация и
context— то, вокруг чего строится большинство вопросов и задач. - Идиоматичность важнее «умных» решений. Ценят «Go way»: простой явный код, обработку ошибок значениями, отсутствие лишних абстракций.
- Чтение кода и ловушки. Часто дают сниппет с вопросом «что выведет и почему» — на слайсах, интерфейсах, defer.
- Инструменты рядом. Ждут знания
go test, флага-race, профилирования и табличных тестов.
Интервьюер проверяет три уровня: знаете базу, понимаете, как оно работает внутри (планировщик, слайс-хедер, устройство интерфейса), и умеете применять идиоматично. Третий уровень и отличает middle от junior.
Что спрашивают и на каких грейдах
Темы распределены неравномерно: конкурентность забирает львиную долю, и её пропуск чаще всего стоит оффера. Ниже — два среза плюс распределение по грейдам.
Доля тем на Go-собеседованиях
Иллюстративно, по разборам собеседований — сверьте с вашими данными
Как часто тема всплывает
Доля собеседований, где тема встречается (иллюстративно)
Из чего состоит секция по грейдам
Условное распределение: теория · задачи · дизайн (иллюстративно)
| Грейд | Что проверяют | Типичные вопросы |
|---|---|---|
| Junior | Синтаксис, базовые горутины и каналы, слайсы, ошибки | Чем горутина отличается от потока; буферизованный vs небуферизованный канал |
| Middle | Внутренности, гонки, context, интерфейсы, defer-ловушки | Как работает планировщик; почему «typed nil» не равен nil; slice aliasing |
| Senior | Архитектура, производительность, escape-анализ, дизайн под нагрузку | Как спроектировать пайплайн; когда значение уходит в кучу; тюнинг GC |
Готовьтесь по реальным вопросам
ENIGMA — нейросеть для собеседований: подсказки в реальном времени и разбор ответов, чтобы выходить на секцию уверенно.
Горутины и планировщик
Главная тема. Горутина — это лёгкий поток исполнения, которым управляет не ОС, а рантайм Go. Стартовый стек — порядка пары килобайт, он растёт по мере надобности, поэтому горутин можно держать сотни тысяч. Запуск — go f().
Модель G-M-P
Планировщик мультиплексирует множество горутин (G) на ограниченное число потоков ОС (M) через логические процессоры (P), которых GOMAXPROCS штук. Каждый P держит локальную очередь горутин; при простое P «крадёт» работу у других (work stealing). Отсюда сильный ответ на «чем горутина отличается от потока»: тысячи горутин живут на горстке потоков, переключение дешёвое и происходит в пользовательском пространстве.
Утечки горутин
Любимый вопрос на middle. Горутина, заблокированная на канале, который никто не закроет и в который никто не пошлёт, живёт вечно и течёт по памяти. Правильный ответ включает context для отмены и аккуратное закрытие каналов — кто пишет, тот и закрывает.
sync.Mutex или atomic часто проще и быстрее канала.
Каналы и select
Небуферизованный канал (make(chan int)) синхронный: отправка блокируется, пока кто-то не прочитает. Буферизованный (make(chan int, 8)) принимает значения до заполнения буфера. Это первый вопрос по теме.
| Операция | Результат |
|---|---|
| Отправка в закрытый канал | Паника |
| Чтение из закрытого канала | Нулевое значение и ok == false |
Чтение/отправка в nil-канал | Блокировка навсегда |
Закрытие nil или уже закрытого | Паника |
range по каналу | Читает, пока канал не закроют |
select ждёт первый готовый case; если готовых несколько — выбирает случайный; с default не блокируется вовсе. На этом строятся таймауты (case <-time.After(d)), неблокирующие отправки и отмена через <-ctx.Done().
Синхронизация и context
Когда каналы избыточны, в ход идёт пакет sync:
sync.Mutex/sync.RWMutex— взаимное исключение;RWMutexразводит много читателей и одного писателя.sync.WaitGroup— дождаться завершения группы горутин (Add,Done,Wait).sync.Once— гарантированно однократная инициализация.sync/atomic— безблокировочные счётчики и флаги; быстрее мьютекса на простых операциях.
context
context.Context — стандартный способ передать отмену, дедлайн и request-scoped значения через цепочку вызовов. Идиома: ctx — первым аргументом функции, отмена распространяется вниз по дереву вызовов, а по <-ctx.Done() горутины корректно завершаются. Это ключ к тому, чтобы горутины не текли.
map роняют программу с фатальной ошибкой (не паникой, которую можно поймать). Нужно — sync.RWMutex поверх мапы или sync.Map под специфичные сценарии.
Слайсы и мапы
Устройство слайса
Слайс — это заголовок из трёх полей: указатель на массив, длина (len) и ёмкость (cap). Отсюда всё поведение. append дописывает в пределах cap, а при нехватке — выделяет новый массив с запасом и копирует данные. Именно поэтому переслайсинг «делит» массив, и запись через один слайс может неожиданно изменить другой (подробный разбор — в задаче 3).
copy(dst, src) копирует поэлементно; трёхиндексный слайс s[low:high:max] ограничивает cap и заставляет следующий append аллоцировать заново — частый способ обезопасить себя от алиасинга.
Мапы
- Порядок итерации намеренно случайный — полагаться на него нельзя.
- Чтение из
nil-мапы возвращает нулевое значение, а запись — паникует. - Ключи должны быть сравнимыми (comparable); слайс ключом быть не может.
- Для конкурентного доступа — синхронизация (см. выше).
Интерфейсы
Интерфейсы в Go удовлетворяются неявно: тип реализует интерфейс, если у него есть нужные методы — без ключевого слова «implements». Пустой интерфейс any (он же interface{}) принимает любое значение.
Ключ к каверзным вопросам — устройство: значение интерфейса это пара (тип, значение). Интерфейс равен nil, только когда обе части пусты. Если положить в интерфейс nil-указатель конкретного типа, интерфейс станет не-nil — это знаменитая ловушка «typed nil» (разбор — в задаче 4).
Проверка и извлечение типа — через утверждение v, ok := i.(T) и switch по типу:
switch v := x.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Println("другой тип")
}
Ошибки и defer
Ошибка в Go — обычное значение: интерфейс error с методом Error() string. Их возвращают и проверяют явно, а не бросают. Что спрашивают:
- Оборачивание.
fmt.Errorf("...: %w", err)сохраняет исходную ошибку в цепочке; развернуть — черезerrors.Is(сравнение с sentinel) иerrors.As(приведение к типу). - Sentinel-ошибки.
var ErrNotFound = errors.New("not found")и сравнение черезerrors.Is. - panic/recover. Для действительно исключительных ситуаций, не для управления потоком.
recoverработает только внутриdefer.
defer
Отложенные вызовы выполняются в порядке LIFO при выходе из функции. Два нюанса, на которых ловят: аргументы defer вычисляются в момент объявления (а не выполнения), и defer может менять именованные возвращаемые значения:
func f() (result int) {
defer func() { result++ }() // изменит возврат
return 10 // вернётся 11
}
Память, GC и ресиверы
Стек, куча и escape-анализ
Где разместить значение — на стеке или в куче — решает компилятор через escape-анализ. Если ссылка «убегает» за пределы функции (возвращается указатель, значение кладётся в интерфейс или захватывается замыканием горутины), оно уходит в кучу. Посмотреть решения компилятора можно так: go build -gcflags="-m". Это частый senior-вопрос: не «что такое куча», а «когда и почему значение туда попадёт».
Сборщик мусора
GC в Go — конкурентный, трёхцветный mark-and-sweep, без поколений и без компактизации, заточенный под короткие паузы (обычно доли миллисекунды stop-the-world). Частоту регулирует GOGC. Хороший ответ — про компромисс между частотой сборок и потреблением памяти, а не пересказ терминов.
Ресиверы значения и указателя
Метод с указателем-ресивером (func (c *Counter)) может менять получателя и не копирует его; со значением (func (c Counter)) работает с копией. Отсюда правило про множества методов: интерфейс, требующий метод с указателем-ресивером, удовлетворяет только *T, но не значение T.
| Аспект | Значение (T) | Указатель (*T) |
|---|---|---|
| Меняет получателя | Нет (копия) | Да |
| Копирование при вызове | Да | Нет |
| Когда выбирать | Мелкие неизменяемые типы | Изменение состояния, большие структуры |
Дженерики
С версии 1.18 в Go есть обобщённые типы: параметры типа в квадратных скобках и ограничения (constraints). В 2026-м базовое владение ими ждут почти везде.
func Map[T, U any](in []T, f func(T) U) []U {
out := make([]U, len(in))
for i, v := range in {
out[i] = f(v)
}
return out
}
Ограничение any разрешает любой тип, comparable — типы, которые можно сравнивать через ==, а собственные ограничения-интерфейсы задают допустимый набор типов. Сильный ответ на «когда применять дженерики»: когда иначе пришлось бы дублировать код под разные типы или городить any с приведениями — но не ради самих дженериков там, где хватает интерфейса.
Практические задачи с решением
Шесть задач, которые реально дают на лайвкодинге Go-разработчику. Сначала попробуйте сами, потом сверьтесь.
Задача 1. Пул воркеров (junior/middle)
Обработать поток заданий фиксированным числом горутин и собрать результаты.
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- j * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ { // 3 воркера
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs) // сигнал воркерам: заданий больше нет
wg.Wait()
close(results) // безопасно закрыть после того, как все воркеры вышли
for r := range results {
fmt.Println(r)
}
}
Разбор. Ключевое — порядок закрытия: close(jobs) останавливает range в воркерах, wg.Wait() ждёт их завершения, и только потом закрывается results. Закрыть results раньше — паника при отправке; не закрыть — range по нему повиснет.
Задача 2. Гонка данных на счётчике (middle)
Сто горутин по 1000 инкрементов общего счётчика. Ожидаем 100000, стабильно получаем меньше. Почему и как починить?
type Counter struct {
value int
}
func (c *Counter) Inc() { c.value++ } // гонка: чтение-инкремент-запись не атомарны
Разбор. c.value++ — три операции без синхронизации, горутины перетирают результаты. Чинится мьютексом:
type Counter struct {
mu sync.Mutex
value int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}
Для простого счётчика быстрее sync/atomic (atomic.AddInt64) — без блокировки. А найти гонку помогает встроенный детектор: go test -race или go run -race.
Задача 3. Слайсы: что выведет код? (middle)
Классическая ловушка на понимание слайс-хедера.
s := []int{1, 2, 3, 4}
low := s[:2] // len 2, cap 4 — общий массив с s
low = append(low, 99) // места хватает → пишем в s[2]
fmt.Println(s) // что здесь?
Разбор. Выведет [1 2 99 4]. У low ёмкость 4, поэтому append не выделяет новый массив, а пишет прямо в общий с s — и «портит» исходный слайс. Фикс — ограничить ёмкость трёхиндексным слайсом:
low := s[:2:2] // len 2, cap 2
low = append(low, 99) // теперь append выделит новый массив
fmt.Println(s) // [1 2 3 4] — не тронут
Задача 4. Ловушка «typed nil» (middle/senior)
Почему проверка на nil не срабатывает?
type MyErr struct{}
func (e *MyErr) Error() string { return "boom" }
func do() error {
var e *MyErr // nil-указатель
return e // возвращаем его как error
}
func main() {
err := do()
fmt.Println(err == nil) // что напечатает?
}
Разбор. Напечатает false. Интерфейс error хранит пару (тип, значение); здесь тип = *MyErr, значение = nil, а значит сам интерфейс не nil. Фикс — возвращать нетипизированный nil явно:
func do() error {
var e *MyErr
if e == nil {
return nil // чистый nil-интерфейс
}
return e
}
Задача 5. Слияние каналов (fan-in) (middle/senior)
Собрать несколько входных каналов в один. Заодно — на дженериках.
func merge[T any](chans ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
wg.Add(len(chans))
for _, c := range chans {
go func(c <-chan T) { // c параметром — без захвата переменной цикла
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() {
wg.Wait()
close(out) // закрываем, когда все входы вычитаны
}()
return out
}
Разбор. На каждый вход — своя горутина, все пишут в общий out. Отдельная горутина ждёт wg.Wait() и закрывает out, чтобы получатель мог спокойно делать range. Передача c параметром важна для версий до Go 1.22, где переменная цикла была общей.
Задача 6. Отмена группы задач: errgroup + context (senior)
Запустить N задач параллельно, отменить остальные при первой ошибке и дождаться всех.
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url // до Go 1.22: копия переменных цикла
g.Go(func() error {
data, err := fetch(ctx, url)
if err != nil {
return err // первая ошибка отменит ctx для остальных
}
results[i] = data // индексы не пересекаются — гонки нет
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Разбор. errgroup.WithContext даёт группу и производный ctx: как только одна горутина вернёт ошибку, ctx отменяется, и остальные, если слушают ctx.Done(), завершаются. Запись в разные индексы results безопасна — горутины не пересекаются по памяти.
errgroup и «без гонок» — будьте готовы объяснить, почему именно без гонок, и запустить с -race. Усиливать формулировки можно, выдумывать — нет.
Чек-лист подготовки
- Закройте конкурентность. Горутины и планировщик, каналы и
select,syncиcontext, утечки горутин — это ядро. - Выучите ловушки. Slice aliasing, «typed nil»,
deferс именованными возвратами, случайный порядок мапы, паника на закрытом канале. - Поймите внутренности. Слайс-хедер, устройство интерфейса, escape-анализ, ресиверы значения и указателя.
- Подтяните ошибки. Оборачивание
%w,errors.Is/As, sentinel-ошибки, panic/recover. - Освойте дженерики на базовом уровне и понимайте, когда они уместнее интерфейса.
- Решайте вслух и запускайте с
-race: проговаривайте план, разбор случаев и сложность — именно это оценивают.
Частые вопросы
Что чаще всего спрашивают на собеседовании по Go?
Конкурентность (горутины, каналы, sync, context), устройство слайсов и мап, интерфейсы, обработка ошибок и defer, а также идиоматичные подводные камни. На конкурентность приходится большинство вопросов и задач.
Чем горутина отличается от потока ОС?
Горутина легче: стартовый стек — пара килобайт против мегабайтов у потока, а управляет ими рантайм Go, мультиплексируя тысячи горутин на горстку потоков через модель G-M-P. Переключение дешёвое и происходит в пользовательском пространстве.
Нужны ли дженерики на Go-собеседовании в 2026?
Да, на базовом уровне: параметры типа, ограничения any и comparable, понимание, когда дженерики уместнее интерфейса или дублирования кода. Глубокой теории обычно не требуют, но владение ждут.
Как подготовиться к собеседованию по Go?
Двигайтесь по частоте тем: сначала конкурентность и ловушки, затем внутренности (слайсы, интерфейсы, escape-анализ) и ошибки. Решайте задачи вслух и запускайте с -race, а не «в уме».
Дают ли задачи на Go-собеседовании?
Да, почти всегда лайвкодинг: пулы воркеров и пайплайны на каналах, синхронизация, а также сниппеты «что выведет код» на слайсах, интерфейсах и defer. Оценивают не только результат, но и ход мысли.
Коротко
Собеседование по Go — это про глубину при маленьком синтаксисе. Решают конкурентность, понимание внутренностей и знание идиоматичных ловушек, а не заученные определения. Закройте горутины, каналы, слайсы и интерфейсы, отработайте разбор кода вслух — и результат становится предсказуемым на любом грейде.
Прогоните собеседование перед реальным
Тренажёр с реальными вопросами по Go, разбор ответов и слабых мест — чтобы прийти на секцию готовым.
Нейросеть для собеседований и подготовки к интервью — подсказки в реальном времени и тренажёр: enigmai.ru
// материал носит справочный характер · цифры в графиках иллюстративные