Go на собеседовании: 9 ловушек и как их обойти

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

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

Начать курс

Введение: Почему Go и почему так сложно?

Go (или Golang) — язык, который обожают за простоту и ненавидят за ту же простоту. На собеседованиях по Go редко спрашивают «напиши сортировку пузырьком» (хотя и такое бывает). Чаще всего вас ждут задачи на конкурентность, понимание устройства памяти и умение писать чистый, идиоматичный код. Если вы привыкли к Python или Java, Go может показаться слишком строгим. Но именно эта строгость и делает его идеальным языком для production-систем.

В этой статье мы разберём ключевые темы, которые обязательно всплывут на собеседовании. Вы получите не просто теорию, а конкретные примеры кода и объяснение «под капотом». Поехали!

1. Горутины и каналы: база, которую нужно знать наизусть

Горутины — это не потоки

Самый частый вопрос: «Чем горутина отличается от потока ОС?». Ответ: горутина легковесна (стек от 2 КБ), управляется планировщиком Go, а не ядром. Миллион горутин — нормально. Миллион потоков — убьёт систему.

// Пример: запуск 10 горутин
for i := 0; i < 10; i++ {
    go func(id int) {
        fmt.Printf("Горутина %d работает\n", id)
    }(i)
}
// Ждём (плохой способ, только для примера)
time.Sleep(time.Second)

Каналы: синхронизация без боли

Каналы в Go — это типизированные «трубы» для передачи данных между горутинами. Запомните три правила:

  • Запись в закрытый канал — паника.
  • Чтение из закрытого канала — возвращает zero value.
  • Небуферизированный канал блокирует горутину до появления получателя.
// Правильная работа с каналом
ch := make(chan int, 2) // буферизированный

ch <- 1
ch <- 2
close(ch)

for val := range ch {
    fmt.Println(val) // 1, 2
}

Совет для собеседования: если спросят про «select», покажите пример с таймаутом:

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

2. Интерфейсы: не просто методы, а контракты

В Go интерфейсы реализуются неявно. Это значит, что структура автоматически удовлетворяет интерфейсу, если у неё есть все нужные методы. Часто спрашивают: «В чём отличие interface{} от any?» — ответ: ни в чём, это синонимы (с Go 1.18).

Типичная задача на собеседовании

Напишите функцию, которая принимает io.Writer и записывает в него строку. Это проверяет понимание интерфейсов из стандартной библиотеки.

func WriteHello(w io.Writer) {
    _, err := w.Write([]byte("Hello, Go!"))
    if err != nil {
        log.Fatal(err)
    }
}

// Использование:
var buf bytes.Buffer
WriteHello(&buf)
fmt.Println(buf.String()) // Hello, Go!

Ловушка: все интерфейсы в Go — это два указателя (тип + значение). Пустой интерфейс (interface{}) не равен nil, если в нём лежит (*int)(nil). Запомните это, чтобы не провалить вопрос про nil != nil.

3. Дженерики: новое, но уже обязательное

С Go 1.18 дженерики стали частью языка. На собеседовании могут попросить написать обобщённую функцию. Например, функцию, которая принимает срез любого типа и возвращает его длину (тривиально) или находит максимум.

// Дженерик-функция для поиска максимума
func Max[T constraints.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Использование:
fmt.Println(Max(10, 20))     // 20
fmt.Println(Max(3.14, 2.71)) // 3.14

Обратите внимание на пакет constraints — он часто используется в вопросах. Но помните: с Go 1.21 его заменили на встроенные cmp.Ordered.

4. Обработка ошибок: не игнорируйте их

В Go нет try-catch. Ошибки — это значения. Типичный код:

f, err := os.Open("file.txt")
if err != nil {
    return fmt.Errorf("не могу открыть файл: %w", err)
}
defer f.Close()

Что спросят:

  • Разница между %v и %w в fmt.Errorf (обёртывание ошибки с сохранением цепочки).
  • Как проверить тип ошибки? — errors.As и errors.Is.
  • Зачем нужен defer? — для закрытия ресурсов, даже если функция паникует.

5. Память и указатели: где живёт ваша переменная?

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

func createUser() *User {
    u := User{Name: "Alice"} // выделяется на стеке?
    return &u               // но указатель уходит — escape в кучу
}

Совет: не пытайтесь микрооптимизировать. Go-компилятор умнее вас. Пишите чистый код, а профилировщик покажет узкие места.

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

Вопрос, который разделяет junior и middle. Правило:

  • Используйте pointer receiver ( (s *Struct) ), если метод изменяет структуру или структура большая (чтобы избежать копирования).
  • Используйте value receiver ( (s Struct) ), если структура маленькая и неизменяемая.
type Counter struct {
    Value int
}

// Pointer receiver — изменяет состояние
func (c *Counter) Increment() {
    c.Value++
}

// Value receiver — только читает
func (c Counter) GetValue() int {
    return c.Value
}

Ловушка: если у структуры есть хотя бы один pointer receiver, все остальные методы тоже должны быть pointer receiver — иначе можно получить неожиданное поведение при копировании.

7. Стандартная библиотека: ваш лучший друг

На собеседовании редко спрашивают про сторонние фреймворки (Gin, Echo).

Блоги

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