Go · Подготовка к собеседованию
Вопросы на собеседовании по Go: короткие ответы
Короткий ответ из каждого урока линии — тот же текст, что в уроке, только без подписи «Короткий ответ:», — и вопросы из его раздела «Дальше спросят». Ответы на эти вопросы, механизм и замеры, на которых всё это стоит, — в уроке по ссылке.
01
Структуры и методы
Структура — составной тип-значение из именованных полей, которые лежат в памяти подряд. Присваивание и передача копируют её целиком, но копия поверхностная: срезы, карты и указатели внутри остаются общими, и
slices.Cloneсmaps.Cloneэтого не меняют — они копируют один уровень. Конструкторов нет — нулевое значение структуры состоит из нулевых значений полей, и хороший тип работает прямо с ним.==разрешён, если сравнимы все поля; поле-интерфейс компилятор пропускает, а паника приходит при выполнении. Методы объявляются на определённом типе своего пакета и по сути — функции с получателем первым аргументом.Дальше спросят
02
Указатели
Указатель — значение, в котором лежит адрес переменной. В Go всё передаётся по значению, указатель тоже: функция получает копию адреса. Изменение через указатель (
*p = 42) видно вызывающему, потому что обе копии ведут к одной переменной; присваивание самому параметру (p = nil) — нет, потому что меняется копия.Дальше спросят
03
Рефлексия
Рефлексия в Go — это пакет
reflect, который по значению в интерфейсе даёт его тип (reflect.Type) и само значение (reflect.Value). Читать можно всегда — неэкспортируемые поля тоже, черезInt(),String()и подобные методы, но не черезInterface().SetиSetIntработают, только еслиCanSetконкретногоValue—true: значение адресуемо и получено не через неэкспортируемое поле; поэтому, чтобы заменить саму переменную, передают указатель и зовутElem(). Элемент среза и запись в карту доступны и без указателя — уIndexиSetMapIndexсвои условия. Рефлексией пользуютсяencoding/json,fmtи всё, что работает с неизвестными при компиляции типами; цена — прежде всего поиск по имени, и библиотеки кешируют его по типу.Дальше спросят
04
io.Reader и io.Writer
io.Reader— интерфейс с одним методомRead(p []byte) (n int, err error): он кладёт вpдоlen(p)байт и возвращает, сколько положил, и ошибку.io.Writer— то же для записи, но, записав меньшеlen(p), обязан вернуть ошибку, а ошибку проверяют и при полной записи. Читая, сначала обрабатываютnбайт, потом смотрят наerr, аio.EOF— это нормальный конец потока. Для типовых задач есть готовыеio.Copy,io.ReadFull,bufio.Дальше спросят
05
Инструменты go
go buildкомпилирует и оставляет бинарник,go runсобирает во временный каталог без отладочной информации и запускает; обе берут пакеты из кеша сборки, ключ которого — содержимое входов.gofmtприводит код к единственному формату, и спорить с ним не принято.go vetловит ошибки, которые компилятор пропускает, — неверный глаголPrintf, копию мьютекса, недостижимый код;go testзапускает только часть его проверок, поэтомуgo vet ./...в CI — отдельный шаг.Дальше спросят
06
Функции и замыкания
Функция в Go — значение: у неё есть тип, нулевое значение
nil, её передают и возвращают; сравнивать можно только сnil. Замыкание — функциональный литерал, который ссылается на переменные объемлющей функции, и эти переменные общие: замыкание видит их текущее значение в момент вызова, а не копию на момент создания.Дальше спросят
07
Пакеты и проект
Пакет — каталог файлов с одним именем в строке
package. Снаружи видны только имена с заглавной буквы. Инициализация идёт так: сначала пакеты, которые импортированы, — каждый один раз; внутри пакета — переменные по зависимостям, затем всеinit()в порядке файлов; и только потомmain.Дальше спросят
08
Модули и зависимости
Модуль — набор пакетов, которые выпускаются под одной версией;
go.modв его корне перечисляет требования — модуль и версию «не ниже», — аgo.sumхранит хеши архива иgo.modкаждого нужного модуля. Версию выбирает алгоритм минимальной версии: для каждого модуля — наибольшая из требуемых в графе, а не последняя в прокси.Дальше спросят
09
JSON и теги
В JSON выходят экспортируемые поля, ключ — имя поля или тег
json:"…".omitemptyпропускаетfalse,0,"",nilи срезы, карты и массивы нулевой длины, но не структуры — поэтомуtime.Time{}выходит как"0001-01-01T00:00:00Z".Unmarshalищет поля без учёта регистра, лишние ключи молча пропускает и не трогает поля, которых во входе нет.Дальше спросят
10
Паттерны конкурентности
Пул воркеров — N горутин, которые читают задачи из одного канала и пишут результаты в другой; канал задач закрывает раздатчик, канал результатов — отдельная горутина после
wg.Wait(). Для задачи на процессоре одновременно работают не большеGOMAXPROCSворкеров, поэтому прибавки сверх этого ждать неоткуда; для задачи, которая ждёт, число ограничивает внешний ресурс, и его ограничивают семафором. Конвейер — стадии через каналы, каждая закрывает свой выход; чтобы стадии не утекали, когда потребитель ушёл, каждое ожидание стадии — и получение, и отправка — стоит вselectсctx.Done().Дальше спросят
11
Срезы
Срез не владеет данными — это окно над чужим массивом, и два окна могут смотреть в одни и те же ячейки. Поэтому запись через один срез видна во втором, а
appendв одних случаях дописывает в тот же массив, в других переезжает в новый — и именно поэтому он возвращает срез, а результат присваивают обратно.Дальше спросят
- Если
appendможет писать в тот же массив, зачем он вообще возвращает срез? - Почему тогда не растить запас всегда вдвое — так ведь меньше копирования?
- Правда ли, что
copyбыстрее, чемappendв цикле? - Что будет, если делать
appendк срезу внутри цикла по нему же? - Почему
capиногда оказывается числом, которого никто не просил? - Срез это ссылочный тип?
- Если
12
Карты
Карта — это пары «ключ — значение» с доступом по ключу, и отсутствующий ключ даёт не ошибку, а нулевое значение; отличить его от настоящего нуля позволяет форма
v, ok := m[k]. Добавьте вторым предложением то, что спрашивают сразу следом: в переменной лежит указатель на структуру рантайма — поэтому карта, переданная в функцию, меняется у вызывающей стороны, и поэтомуnil-карту можно читать, но не писать.Дальше спросят
- Раз карта — указатель, зачем тогда
make? Почему нельзя просто объявить переменную? - Почему нельзя взять адрес элемента карты?
- Если карта не уменьшается, как чистить долгоживущий кеш?
- Почему промах дороже попадания — разве не наоборот?
- Можно ли подобрать ключи так, чтобы карта выродилась в список?
- Когда брать
sync.Mapвместо карты с мьютексом?
- Раз карта — указатель, зачем тогда
13
Строки и руны
Строка в Go — неизменяемая последовательность байтов, а не символов, и корректный UTF-8 в ней не обязателен. Поэтому
lenсчитает байты, аs[i]даёт байт;for rangeдекодирует UTF-8 и отдаёт кодовые точки с байтовыми смещениями, а на некорректном байте —U+FFFDи шаг в один байт. Устройство добавьте отдельно и как свойство реализации: в gc строка — указатель и длина, подстрока не копирует и потому удерживает весь массив, а+=в цикле каждый раз копирует накопленное, отсюда квадратичная работа.Дальше спросят
14
Интерфейсы
Интерфейс — это список методов, и тип реализует его молча, просто потому что нужные методы у него есть. Отсюда две вещи, которые и спрашивают дальше: метод с получателем-указателем не входит в набор методов значения, а сама интерфейсная переменная несёт пару «динамический тип и динамическое значение» — и равна
nil, только когда пусты обе половины.Дальше спросят
- Можно ли сравнивать интерфейсы через
==? - Где объявлять интерфейс — рядом с реализацией или у потребителя?
- Зачем нужен пустой интерфейс и чем он отличается от
any? - Что такое
itabи когда он создаётся? - Как проверить на этапе компиляции, что тип реализует интерфейс?
- Интерфейс со значением или с указателем — что класть?
- Можно ли сравнивать интерфейсы через
15
Получатель метода
Метод с получателем-значением работает с копией, а метод с получателем-указателем может изменить исходное значение. Получатель — это первый аргумент метода, записанный слева, а аргументы копируются; отсюда и «метод ничего не изменил», и «тип не реализует интерфейс».
Дальше спросят
- Что будет, если у типа половина методов со значением, а половина с указателем?
- Можно ли объявить метод на срезе или на карте?
- Что говорит go vet про копирование мьютекса?
- Метод можно вызвать на nil-указателе?
- Как получатель связан с ловушкой типизированного nil?
- Стоит ли делать получатель-указатель ради «оптимизации» маленькой структуры?
16
Встраивание
Наследования в Go нет. Есть встраивание — поле без имени, чьи поля и методы можно вызывать напрямую;
s.Log()— сокращение дляs.Logger.Log(). Это композиция: внешний тип содержит встроенный, а не является им, и метод встроенного типа работает со своим значением, а не с внешним.Дальше спросят
17
Ошибки
Ошибка в Go — обычное значение, которое возвращают и передают вверх руками. Функция вернула, вызывающий добавил контекст и передал дальше, наверху кто-то один принял решение. Всё остальное в теме — про то, сохранилась ли при добавлении контекста ссылка на исходную ошибку: с ней наверху можно классифицировать причину, без неё остаётся только текст в логе.
Дальше спросят
18
defer, panic, recover
У отложенного вызова три момента, а не один. Регистрация и вычисление аргументов — в момент
defer, выполнение — при выходе из функции; а если отложенных вызовов несколько, они идут в обратном порядке. Паника разворачивает стек, выполняя по дороге отложенные вызовы каждого кадра, и перехватить её можно только вызовомrecoverнепосредственно в отложенной функции.Дальше спросят
19
Context
Контекст переносит по дереву вызовов отмену, срок и значения, привязанные к запросу — чтобы работу, которая больше никому не нужна, можно было остановить, а не доводить до конца. Отмена при этом сигнал, а не прерывание: она действует только там, где
Done()читают. И связь в дереве односторонняя — вниз отмена идёт, вверх и вбок нет.Дальше спросят
20
Планировщик
Горутина — не поток операционной системы, а единица работы самой программы на Go. Её запускает одно слово
go, стоит она килобайты против мегабайтов у потока, а какая из них сейчас на процессоре — решает планировщик внутри процесса. Поэтому горутин бывают тысячи при единицах потоков, и поэтомуGOMAXPROCSограничивает не число горутин, а число выполняющихся одновременно.Дальше спросят
21
Каналы
Канал — это связь двух горутин, в которой отправка и приём встречаются: пока получателя нет, отправитель ждёт, и наоборот. Буфер это ожидание не убирает, а отсрочивает ровно на свою вместимость. Из закрытого канала читать можно — сначала дочитываются положенные значения; писать в него и закрывать его повторно нельзя. А ветки
selectвыбираются равномерно случайно, а не сверху вниз.Дальше спросят
22
sync и atomic
n++— это три действия, а не одно: прочитать, прибавить, записать; другая горутина успевает вклиниться между ними, и обновление теряется. Дальше выбор простой: мьютекс защищает целостность участка кода — сколько угодно полей и любую логику между ними, — а атомарная операция годится там, где состояние ограничено и меняется одним неделимым действием: счётчик, флаг, указатель. И то и другое даёт не скорость, а гарантию того, что одна горутина увидит работу другой целиком, а не наполовину.Дальше спросят
23
Гонки данных
Гонка данных — это когда две горутины обращаются к одной ячейке памяти, хотя бы одна пишет, и между обращениями нет синхронизации. Лечится мьютексом, каналом или атомарной операцией. Находится флагом
-race:go test -race, и в CI он должен быть.Дальше спросят
24
Стек и куча
В Go программист не выбирает между стеком и кучей — выбор делает компилятор, и вопрос у него один: переживёт ли значение свой кадр. Если доказать, что не переживёт, нельзя — куча. Поэтому возврат указателя на локальную переменную законен, а
&в коде сам по себе о размещении не говорит ничего.Дальше спросят
25
Сборщик мусора
Сборщик возвращает память объектов, до которых от корней больше нет пути, и делает это одновременно с программой — останавливая её дважды за цикл и ненадолго. Поэтому пауза короткая и от размера кучи не зависит: у меня 16 и 256 мегабайт живых данных дали 39 и 48 микросекунд — рост в 1,22 раза при шестнадцатикратном росте кучи. Растёт при этом не пауза, а работа разметки — доля процессорного времени, и путают с паузой именно её.
Дальше спросят
- Что такое трёхцветная разметка и зачем нужен барьер записи?
- Зачем нужен runtime.GC(), если сборщик работает сам?
- Как понять, что программа тратит время именно на сборку?
- Почему RSS процесса больше, чем показывает HeapAlloc?
- Финализаторы — можно ли на них полагаться?
- Что изменится, если поставить GOGC=off?
26
Профилирование
Снять профиль CPU — из бенчмарка через
go test -cpuprofileили из сервиса черезnet/http/pprof— и открыть вgo tool pprof -top. Профилировщик сэмплирующий, поэтому наверху окажутся функции, где программа проводит больше всего времени, а короткие вызовы могут не попасть вовсе. Частоту — 100 раз в секунду в go1.24.7 — называют как деталь реализации, а не как свойство языка.Дальше спросят
27
Тестирование
Тест — функция
TestXxx(t *testing.T), табличный тест — срез случаев и одна проверка в цикле, каждый случай — подтестt.Run, его можно запустить отдельно через-run.t.Parallelпускает подтесты параллельно, а освобождать общие ресурсы надо черезt.Cleanup. Гонки ищутgo test -race, кеш обходят-count=1.Дальше спросят
28
HTTP-сервер
Обработчик — интерфейс
http.Handler,ServeMuxраскладывает запросы по путям, промежуточный слой — функцияHandler → Handler. Сервер обслуживает каждое соединение в своей горутине (в HTTP/2 — каждый поток), поэтому общее изменяемое состояние обработчиков нужно защищать. В продакшене — свойhttp.Serverс таймаутами и остановка черезShutdown.Дальше спросят