Как пройти собеседование по Go: топ вопросов продвинутого уровня

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Продвинутое собеседование по Go: от горутин до дженериков

Собеседование на позицию Go-разработчика среднего или старшего уровня — это не просто проверка синтаксиса. Работодатели ожидают глубокого понимания модели конкурентности, работы рантайма, управления памятью и особенностей типизации. В этой статье мы разберём топ продвинутых вопросов по Golang, которые помогут вам уверенно пройти техническое интервью. Мы не будем останавливаться на основах вроде «что такое срез» — перейдём сразу к сложным темам с примерами кода.

1. Горутины, каналы и паттерны синхронизации

Вопросы про горутины и каналы — классика любого собеседования. Однако на продвинутом уровне вас спросят не только «что такое горутина», но и о тонкостях работы планировщика (GMP-модель) и правильном использовании примитивов синхронизации.

Вопрос: Как работает планировщик горутин в Go? Что такое G, M, P?

Планировщик Go использует модель GMP: G (goroutine) — лёгкий поток, M (machine) — поток ОС, P (processor) — контекст выполнения. P привязан к логическому процессору и управляет очередью горутин. Когда горутина блокируется (например, на системном вызове), P может переключить M на другую горутину, что обеспечивает эффективную многозадачность без создания тысяч потоков ОС.

Вопрос: В чём разница между буферизированным и небуферизированным каналом? Приведите пример deadlock.

// Небуферизированный канал — блокирует отправителя до получения
ch := make(chan int)
go func() {
    ch <- 42 // блокируется, пока main не прочитает
}()
fmt.Println(<-ch) // 42

// Буферизированный канал — не блокирует, пока буфер не заполнен
chBuf := make(chan int, 2)
chBuf <- 1
chBuf <- 2 // ок, буфер не полон
// chBuf <- 3 // deadlock, если нет читателя

// Типичный deadlock: чтение из пустого небуферизированного канала
chDead := make(chan int)
// <-chDead // fatal error: all goroutines are asleep — deadlock!

Вопрос: Как безопасно закрыть канал? Что такое «range over channel»?

Закрывать канал должен только отправитель. Получатель может использовать второй параметр (v, ok := <-ch) для проверки. Итерация for v := range ch завершится, когда канал будет закрыт. Попытка записи в закрытый канал вызывает панику.

ch := make(chan int)
go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch) // обязательно закрываем
}()
for v := range ch {
    fmt.Println(v) // 0, 1, 2
}

2. Интерфейсы, пустой интерфейс и type assertions

Интерфейсы в Go — это контракты. На собеседовании важно понимать, как они устроены внутри (структура iface и eface) и чем отличается статическая типизация от динамической.

Вопрос: Что такое пустой интерфейс (interface{}) и когда его использовать?

Пустой интерфейс не содержит методов, поэтому любой тип его реализует. Используется для работы с данными неизвестного типа (например, json.Unmarshal). Однако злоупотребление пустым интерфейсом нарушает строгую типизацию Go и может привести к ошибкам времени выполнения.

Вопрос: Как работает type assertion? В чём отличие от type switch?

var val interface{} = "hello"

// type assertion
str, ok := val.(string)
if ok {
    fmt.Println("Это строка:", str)
}

// type switch
switch v := val.(type) {
case string:
    fmt.Println("Строка:", v)
case int:
    fmt.Println("Число:", v)
default:
    fmt.Println("Неизвестный тип")
}

Вопрос: Как интерфейсы хранятся в памяти?

Каждый интерфейс — это два указателя: один на тип (itable), другой на значение (data). Для пустого интерфейса (interface{}) структура проще — только type *Type и data unsafe.Pointer. Понимание этого помогает избежать ловушек с nil интерфейсами.

var p *int = nil
var i interface{} = p
fmt.Println(i == nil) // false! Потому что i содержит информацию о типе

3. Управление памятью: стек, куча и escape analysis

Продвинутый Go-разработчик должен понимать, как компилятор решает, куда поместить переменную — на стек или в кучу, и как это влияет на производительность.

Вопрос: Что такое escape analysis? Как он работает?

Escape analysis — это оптимизация компилятора Go. Если переменная не «убегает» за пределы функции (не возвращается, не передаётся в горутину, не присваивается глобальной переменной), она размещается на стеке, что очень быстро. В противном случае она выделяется в куче, что требует работы сборщика мусора.

// Пример: переменная убегает в кучу
func createUser() *User {
    u := User{Name: "Alice"}
    return &u // u "убегает" — будет в куче
}

// Пример: остаётся на стеке
func sum(a, b int) int {
    result := a + b
    return result // result не убегает
}

Вопрос: Как сборщик мусора (GC) работает в Go? Какие поколения?

Go использует non-generational concurrent mark-sweep сборщик мусора. Он не поколенческий (в отличие от Java или .NET). Сборка происходит конкурентно с приложением, используя три фазы: Mark Setup (STW), Concurrent Marking, Mark Termination (STW). Это минимизирует паузы, но увеличивает нагрузку на CPU.

Вопрос: Как уменьшить нагрузку на GC?

  • Использовать sync.Pool для переиспользования объектов.
  • Избегать большого количества мелких аллокаций в куче.

Блоги

Рекомендаций по книгам