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

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

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

Начать курс

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

Собеседование на позицию Go-разработчика — это не только проверка синтаксиса, но и глубокое понимание модели конкурентности, работы с памятью и идиоматического стиля языка. В этой статье мы разобрали топ-10 вопросов, которые чаще всего задают на собеседованиях по Golang, с подробными ответами и примерами кода. Материал подойдет как для Junior, так и для Middle-уровня.

1. Горутины vs потоки ОС: в чем разница?

Это базовый вопрос, который проверяет понимание модели выполнения Go. Ответ должен быть четким:

  • Размер стека: Горутина стартует с ~2 КБ стека, который динамически растет. Поток ОС имеет фиксированный стек (1-8 МБ).
  • Переключение контекста: Горутины переключаются в пространстве пользователя (Go scheduler). Потоки — через системный вызов (дороже).
  • Стоимость создания: Горутина стоит ~4 КБ памяти, поток — ~1 МБ. Можно запустить тысячи горутин.
// Пример: 10000 горутин
func main() {
    for i := 0; i < 10000; i++ {
        go func(n int) {
            fmt.Println("Горутина", n)
        }(i)
    }
    time.Sleep(time.Second)
}

2. Каналы: буферизированные и небуферизированные

Вопрос на понимание синхронизации. Объясните разницу на примере:

  • Небуферизированный канал: Отправка блокируется до получения. Гарантирует синхронность.
  • Буферизированный канал: Отправка не блокируется, пока буфер не заполнен. Позволяет асинхронную передачу.
// Небуферизированный канал
ch := make(chan int)
go func() { ch <- 42 }()
val := <-ch // Блокируется, пока не придет значение

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

3. Select и обработка множества каналов

Оператор select позволяет ожидать несколько операций с каналами. Часто спрашивают про default и таймауты:

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() { time.Sleep(2 * time.Second); ch1 <- "from ch1" }()
    go func() { time.Sleep(1 * time.Second); ch2 <- "from ch2" }()

    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    case <-time.After(3 * time.Second):
        fmt.Println("Таймаут")
    }
}

4. Интерфейсы: пустой интерфейс и type assertion

Проверка понимания interface{} (any) и безопасного приведения типов:

func describe(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Целое: %d\n", v)
    case string:
        fmt.Printf("Строка: %s\n", v)
    default:
        fmt.Printf("Неизвестный тип: %T\n", v)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe(3.14)
}

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

Вопрос на понимание того, как Go решает, где размещать переменные. Ключевой момент — escape analysis:

  • Если компилятор видит, что переменная может быть использована после возврата из функции, она «убегает» в кучу.
  • Использование указателей, интерфейсов и замыканий часто вызывает аллокацию в куче.
// Переменная x "убегает" в кучу
func createPointer() *int {
    x := 42
    return &x // x не может быть на стеке
}

6. Defer: порядок выполнения и особенности

Defer выполняется по принципу LIFO (Last In, First Out). Аргументы вычисляются в момент объявления, а не выполнения:

func main() {
    x := 10
    defer fmt.Println("defer 1:", x) // выведет 10
    x = 20
    defer fmt.Println("defer 2:", x) // выведет 20
    // вывод: "defer 2: 20", "defer 1: 10"
}

7. Паника и recover: как обрабатывать ошибки

Паника — это аварийная остановка. recover позволяет перехватить панику, но работает только внутри defer:

func safeDivision(a, b int) (result int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
            result = 0
        }
    }()
    if b == 0 {
        panic("деление на ноль")
    }
    return a / b
}

func main() {
    fmt.Println(safeDivision(10, 0)) // 0
}

8. Структуры и методы: value vs pointer receiver

Важный вопрос для понимания мутабельности и производительности:

  • Value receiver — метод работает с копией. Безопасно для горутин, но не изменяет оригинал.
  • Pointer receiver — метод может изменять структуру. Используется для больших структур (экономия памяти).
type Counter struct {
    Value int
}

// Value receiver
func (c Counter) Increment() {
    c.Value++
}

// Pointer receiver
func (c *Counter) IncrementPtr() {
    c.Value++
}

func main() {
    c := Counter{Value: 0}
    c.Increment()      // не изменит
    c.IncrementPtr()   // изменит
    fmt.Println(c.Value) // 1
}

9. sync.WaitGroup и синхронизация горутин

Базовый паттерн ожидания завершения группы горутин:

func main() {
    var wg sync.WaitGroup
    for i := 0; i <                

Блоги

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