Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Junior ~20 мин 10 вопросов

Собеседование Junior Golang: базовые вопросы с ответами

Junior-собеседование по Go проверяет базу: объявление переменных, срезы и карты, defer, обработку ошибок, структуры и интерфейсы. Ниже — типовые вопросы с ответами, которые ждут от джуна. Если по ходу понимаете, что какая-то тема плавает, — пройдите её в базовом курсе Go: каждая тема — отдельный урок с запуском кода в браузере.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

1. Чем var отличается от := ?

Оба объявляют переменную, но := — короткое объявление: работает только внутри функций, требует хотя бы одной новой переменной слева и выводит тип из правой части. var работает везде, включая уровень пакета, позволяет указать тип явно и объявить без инициализации (получится нулевое значение). Внутри функций обычно пишут :=, var оставляют для пакетного уровня и случаев, когда тип важнее значения: var r io.Reader.

var port int          // 0, уровень пакета — только var
name := "ГоМастер"    // короткое объявление внутри функции
var r io.Reader       // тип важнее значения

2. Чем срез отличается от массива?

Массив — значение фиксированной длины: [3]int влезает в тип, копируется целиком при присваивании и передаётся по значению. Срез — «окно» над массивом: структура из указателя на данные, длины len и ёмкости cap. Срез растёт через append (при нехватке cap выделяется новый массив и данные копируются), передаётся дёшево, и именно срезы в Go используются повсеместно. Вопрос-ловушка рядом: после append внутри функции вызывающий может не увидеть новые элементы, если произошёл реаллокация и срез указывает на старый массив — поэтому append перезаписывают в ту же переменную: s = append(s, x).

a := [3]int{1, 2, 3} // массив: длина — часть типа
s := []int{1, 2, 3}  // срез: len=3, cap=3
s = append(s, 4)     // cap вырос, данные скопированы в новый массив

3. Что будет при записи в nil-карту? А чтение?

Запись в nil-карту паникует: «assignment to entry in nil map». Чтение из nil-карты безопасно и возвращает нулевое значение: карта обязана быть «читаемой» без инициализации. Правильное создание — make(map[string]int) или литерал с элементами. Идиома comma-ok: v, ok := m[key] — ok отличает «ключ со значением-нулём» от «ключа нет». Карта не потокобезопасна: для конкурентного доступа — sync.Mutex вокруг или sync.Map для узких сценариев read-heavy.

m := map[string]int{"a": 1}
v := m["нет"]          // 0, паники нет
v, ok := m["a"]        // 1, true
// m2["x"] = 1 // паника: m2 == nil

4. Что делает defer? В каком порядке выполняются отложенные вызовы?

defer откладывает вызов функции до выхода из объемлющей функции — независимо от того, вышли по return, панике или обычному пути. Отложенные вызовы выполняются в порядке LIFO — стек: последний defer сработает первым. Аргументы вычисляются в момент записи defer, а не в момент вызова. Типовое применение — освобождение ресурсов: defer f.Close(), defer mu.Unlock(), defer recover() в обёртках. Ловушка в цикле: defer внутри цикла накопится до конца функции; если файлы нужно закрывать сразу — выносите тело цикла в функцию.

func demo() {
    defer fmt.Println("первый defer")
    defer fmt.Println("второй defer")
    fmt.Println("тело")
}
// тело → второй defer → первый defer

5. Как в Go обрабатывают ошибки? Что такое оборачивание ошибок?

Ошибки — значения: функция возвращает (результат, error), и вызывающий обязан проверять if err != nil. Ошибку создают errors.New или fmt.Errorf, оборачивают через %w: fmt.Errorf("читаю конфиг: %w", err) — так сохраняется исходная ошибка, и её можно распаковать через errors.Is (сравнение с sentinel-ошибкой, например io.EOF) и errors.As (извлечение конкретного типа, например *fs.PathError). Исключений в Go нет: панику используют для программных ошибок программиста, а не для ожидаемых сбоев — «файл не найден» это ошибка, а не паника.

data, err := os.ReadFile(path)
if err != nil {
    return fmt.Errorf("читаю конфиг %s: %w", path, err)
}
if errors.Is(err, fs.ErrNotExist) { /* своя политика */ }

6. Нулевые значения — что это и зачем?

В Go нет «неинициализированных» переменных: каждая имеет нулевое значение своего типа — 0 для чисел, false для bool, пустая строка для string, nil для указателей, срезов, карт, каналов, интерфейсов и функций. Это убирает целый класс багов с чтением мусорной памяти и делает структуры пригодными к использованию сразу после объявления. Знать нулевые значения обязательно: срез nil — len 0 и append работает, карта nil — читать можно, писать нельзя, интерфейс nil — вызов метода на nil-интерфейсе паникует.

7. Чем метод с приёмником-значением отличается от приёмника-указателя?

Метод с приёмником-значением работает с копией: изменения поля внутри метода не видны снаружи, вызов на больших структурах копирует данные. Приёмник-указатель получает адрес и может менять оригинал. Правило выбора: указатель — если метод мутирует состояние, структура большая или в типе уже есть хотя бы один указательный метод (смешивать нежелательно: набор методов типа определяется всеми его методами сразу). На значения-типы вроде time.Duration указательные методы не нужны — они неизменяемы.

type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }      // указатель: меняет оригинал
func (c Counter) Value() int { return c.n } // значение: читает копию

8. Что такое интерфейс и что такое неявная реализация?

Интерфейс — набор методов; тип реализует интерфейс просто потому, что у него есть все нужные методы, — без слова implements и без регистрации. Это позволяет объявлять интерфейс там, где он используется, а не там, где тип. Классика: io.Reader и io.Writer — любой тип с правильными сигнатурами встаёт в цепочку чтения. Специальный случай — пустой интерфейс any (interface{}): ему удовлетворяет любой тип; в новом коде для произвольных значений предпочитают any и дженерики, а не пустой интерфейс.

9. Как работает строка: что вернёт len("привет")?

Строка в Go — неизменяемая последовательность байт в UTF-8. len возвращает число байт, а не символов: len("привет") — 12, потому что каждая кириллическая буква кодируется двумя байтами. Символ — это руна (кодовая точка Unicode, int32); перебор по range отдаёт именно руны с их индексами в байтах, а s[i] — отдельный байт. Обрезка s[:3] может разрезать букву посередине. Для «количества символов» — utf8.RuneCountInString(s).

s := "привет"
fmt.Println(len(s))                    // 12 байт
fmt.Println(utf8.RuneCountInString(s)) // 6 рун

10. Что выведет iota и где её применяют?

iota — счётчик const-блока: в первой строке он 0 и растёт на каждой следующей строке. Типовое применение — перечисления и битовые маски. Вопрос часто идёт с трюком: iota сбрасывается в каждом новом const-блоке и «замораживается», если строка без выражения повторяет предыдущее. Уметь прочитать такой блок — часть вопроса.

type Level int
const (
    New Level = iota // 0
    Active           // 1
    Blocked          // 2
)
const (
    KB = 1 << (10 * (iota + 1)) // 1024
    MB                          // 1048576
)

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: