Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
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 == nil4. Что делает 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 → первый defer5. Как в 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
)Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Язык Go с нуля: базовый синтаксис и конструкции — Интерактивный курс базового синтаксиса Go для начинающих: переменные, циклы, срезы, функции, структуры. 25 уроков с кодом в браузере, тестами и первым приложением.