Основы языка: типы, указатели, замыкания, модули, синхронизация
Кратко о теме
Заголовок раздела «Кратко о теме»Go спроектирован как маленький язык с явной моделью памяти и данных. Практически всё, что спрашивают в блоке «основы», сводится к трём идеям. Первая: в Go всё передаётся по значению — аргументы функций, возвращаемые значения, присваивания. «Передача по ссылке» в Go существует только как жаргон: когда говорят «слайс/мапа/канал передаются по ссылке», имеют в виду, что копируется маленький дескриптор (заголовок слайса из трёх слов, указатель на хеш-таблицу, указатель на hchan), а сами данные лежат отдельно и оказываются общими. Никаких C++-ссылок (int&) в языке нет — есть только указатели, которые сами являются обычными значениями и могут быть nil.
Вторая идея: нулевое значение. Каждый тип имеет чётко определённый zero value, и объявление переменной без инициализатора всегда даёт его: 0 для чисел, false для bool, "" для строк, nil для указателей, слайсов, мап, каналов, функций, интерфейсов и unsafe.Pointer, и покомпонентный ноль для массивов и структур. Отдельного «undefined» или «null, отличного от нуля» в языке нет. Отсюда идиома «make the zero value useful»: var buf bytes.Buffer, var mu sync.Mutex, var wg sync.WaitGroup работают сразу, без конструктора. Обратная сторона — nil-мапа читается, но не пишется; nil-слайс отлично работает в append и len; nil-канал блокирует навсегда; nil-интерфейс с ненулевым типом внутри — классическая ловушка.
Третья идея: типы в Go статические и номинальные, неявных числовых преобразований нет. int и int64 — разные типы, даже когда на amd64 они одного размера; сложить их без явного int64(x) нельзя. Записи вида int64(x) — это не вызов функции, а конверсия типа, синтаксически похожая на вызов. Наследования нет: переиспользование кода строится на встраивании (embedding) и интерфейсах, которые удовлетворяются структурно, без implements.
Модель конкурентности стоит рядом с этим фундаментом: горутины — это не потоки ОС, а сущности рантайма, которые мультиплексируются на потоки по схеме M:N (модель GMP). Планировщик ОС раздаёт кванты потокам, планировщик Go раздаёт горутины логическим процессорам P, число которых задаётся GOMAXPROCS. Для согласования доступа к общей памяти есть два набора инструментов: каналы («share memory by communicating») и классические примитивы из sync/sync/atomic.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Что такое замыкание в Go?
Заголовок раздела «Что такое замыкание в Go?»Коротко. Замыкание — функциональный литерал, который ссылается на переменные из внешней области видимости и «захватывает» их вместе с собой. Функция и захваченное окружение живут как единое значение типа func(...), поэтому замыкание можно передавать, возвращать и вызывать после того, как внешняя функция завершилась.
Глубже. Захват идёт по переменной, а не по значению: замыкание видит изменения и само может их вносить. Если компилятор видит, что переменная переживает кадр функции (escape analysis), она переносится в кучу, а замыкание получает на неё указатель. Само значение функции представлено в рантайме как указатель на funcval — структуру, где лежит адрес кода и следом захваченные переменные; поэтому функции в Go можно сравнивать только с nil.
func counter() func() int { n := 0 // уедет в кучу: переживает counter() return func() int { n++ return n }}
c := counter()fmt.Println(c(), c(), c()) // 1 2 3 (порядок вычисления аргументов слева направо)Что такое go.mod и go.sum ?
Заголовок раздела «Что такое go.mod и go.sum ?»Коротко. go.mod — манифест модуля: путь модуля, требуемая версия языка/тулчейна и список прямых и косвенных зависимостей с версиями. go.sum — файл с криптографическими хешами конкретных версий модулей и их go.mod, нужный для проверки, что скачанный код побайтово тот же, что и раньше.
Глубже. В go.mod встречаются директивы module, go, toolchain, require (косвенные помечаются // indirect), replace, exclude, retract, а также godebug (с Go 1.23). Версии выбираются алгоритмом MVS (minimal version selection): берётся максимум из минимально требуемых версий, никакого «latest» на лету. go.sum — не lock-файл: набор версий фиксирует именно go.mod, а go.sum отвечает только за целостность. Хеши сверяются с прозрачным журналом контрольных сумм sum.golang.org; поведение настраивается переменными GOSUMDB, GONOSUMDB и GOPRIVATE — для внутренних репозиториев правильный способ отключить проверку через публичный журнал именно GOPRIVATE/GONOSUMDB, а не GOFLAGS=-mod=mod. Полезные команды: go mod tidy, go mod verify, go mod why, go mod graph. Оба файла коммитятся в репозиторий.
Task: Write a program that prints numbers from 1 to 100. For multiples of 3, print “Fizz ”instead; for multiples of 5, print “Buzz ”; for multiples of both 3 and 5, print “FizzBuzz ”
Заголовок раздела «Task: Write a program that prints numbers from 1 to 100. For multiples of 3, print “Fizz ”instead; for multiples of 5, print “Buzz ”; for multiples of both 3 and 5, print “FizzBuzz ”»Коротко. Классический FizzBuzz. Главное — проверять кратность 15 (или обоим сразу) первой, иначе ветка %3 перехватит числа вроде 15.
Глубже. Вариант без деления на 15 и без лишних веток — накапливать строку; он лучше масштабируется, если правил станет больше.
package main
import "fmt"
func main() { for i := 1; i <= 100; i++ { switch { case i%15 == 0: fmt.Println("FizzBuzz") case i%3 == 0: fmt.Println("Fizz") case i%5 == 0: fmt.Println("Buzz") default: fmt.Println(i) } }}Расширяемая версия:
for i := 1; i <= 100; i++ { s := "" if i%3 == 0 { s += "Fizz" } if i%5 == 0 { s += "Buzz" } if s == "" { s = strconv.Itoa(i) } fmt.Println(s)}Which ones exist? What are they used for?
Заголовок раздела «Which ones exist? What are they used for?»Коротко. Обрывок исходника, вопрос не восстанавливается: без предыдущей реплики непонятно, о каких сущностях речь.
Глубже. Судя по соседним вопросам в этом же интервью, это уточнение либо к «какие примитивы синхронизации существуют», либо к «какие типы данных есть в Go». Оба разобраны ниже — см. «Какие примитивы синхронизации существуют в Go?» и «Какие знаешь типы в Go?».
Каким образом происходит захват внешних переменных в замыканиях?
Заголовок раздела «Каким образом происходит захват внешних переменных в замыканиях?»Коротко. По переменной (по ссылке), а не по значению: замыкание получает указатель на переменную, поэтому видит её последующие изменения и может менять её само. Если переменная переживает кадр создавшей её функции, escape analysis переносит её в кучу.
Глубже. До Go 1.21 включительно переменная цикла for i := ... была одна на весь цикл, и все замыкания видели её последнее значение — отсюда легендарная ошибка с горутинами в цикле. С Go 1.22 семантика изменена: переменные цикла создаются заново на каждой итерации, и код ниже печатает 0, 1, 2 в произвольном порядке, а не «3 3 3». Модуль обязан объявлять go 1.22 и выше в go.mod, иначе действует старая семантика (это выбирается по языковой версии файла/модуля).
for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Go >= 1.22: 0,1,2 в любом порядке; Go <= 1.21: чаще всего 3,3,3}Чтобы захватить именно значение, его копируют явно: i := i внутри тела или передача параметром go func(i int){...}(i) — так пишут до сих пор ради совместимости со старыми версиями.
Можно ли в Golang использовать динамические библиотеки
Заголовок раздела «Можно ли в Golang использовать динамические библиотеки»Коротко. Да. Через cgo можно линковаться с системными .so/.dylib/.dll, можно собрать сам Go-код как динамическую библиотеку (-buildmode=c-shared), и есть plugin — загрузка Go-кода в рантайме, но с серьёзными ограничениями.
Глубже. По умолчанию Go собирает статический бинарник, но он становится динамически слинкованным, как только включается cgo (например, стандартный резолвер net или os/user на glibc) — отсюда классическая ошибка «работает локально, падает в scratch-образе»; лечится CGO_ENABLED=0 или сборкой с musl. Режимы сборки: -buildmode=c-shared (получить .so + заголовок для вызова из C/Python/Java), -buildmode=c-archive (статический .a), -buildmode=shared (разделяемая библиотека из Go-пакетов, экспериментально), -buildmode=plugin (пакет plugin, только Linux/FreeBSD/macOS). У plugin жёсткие требования: тот же компилятор, те же версии общих зависимостей, одинаковые флаги сборки; выгрузить плагин нельзя. Для чистого вызова C-библиотек без cgo существует сторонний purego.
Стандартные вопросы про Go.
Заголовок раздела «Стандартные вопросы про Go.»Коротко. Обрывок исходника, вопрос не восстанавливается: это пометка кандидата о характере секции, а не конкретный вопрос.
Глубже. Под «стандартными» почти всегда имеются в виду: слайсы (устройство заголовка, append, len/cap), мапы (устройство, конкурентный доступ), интерфейсы (iface/eface, nil-интерфейс), горутины и каналы, defer/panic/recover, GC и escape analysis, context. Готовиться стоит по этому списку.
Можно ли передать функцию как параметр в другую функцию?
Заголовок раздела «Можно ли передать функцию как параметр в другую функцию?»Коротко. Да, функции в Go — граждане первого класса: их можно присваивать переменным, передавать аргументами, возвращать и хранить в структурах, слайсах и мапах.
Глубже. Тип функции определяется сигнатурой: func(int, int) int. Есть методы-значения (v.Method — замыкание с захваченным получателем) и method expressions (T.Method — функция, где получатель становится первым аргументом). На этом построены sort.Slice, http.HandlerFunc, middleware, функциональные опции.
func apply(nums []int, f func(int) int) []int { out := make([]int, len(nums)) for i, n := range nums { out[i] = f(n) } return out}
double := func(x int) int { return x * 2 }fmt.Println(apply([]int{1, 2, 3}, double)) // [2 4 6]Есть ли set в Go?
Заголовок раздела «Есть ли set в Go?»Коротко. Отдельного типа set в языке и в стандартной библиотеке нет. Идиома — map[T]struct{} (нулевой размер значения) или map[T]bool, если удобнее читать код.
Глубже. struct{}{} занимает 0 байт, все такие значения указывают на общую переменную runtime.zerobase, поэтому память тратится только на ключи. map[T]bool удобнее в чтении (if set[x] {} вместо if _, ok := set[x]; ok {}), но занимает по байту на элемент и допускает бессмысленное состояние set[x] == false. В стандартной библиотеке с Go 1.21 есть пакеты slices и maps (slices.Contains, maps.Keys), которые часто закрывают потребность без множества. Ключ должен быть comparable: слайсы, мапы и функции ключами быть не могут.
Есть ли исключения в Go?
Заголовок раздела «Есть ли исключения в Go?»Коротко. Нет. Ошибки — обычные значения типа error, возвращаемые последним результатом. Есть panic/recover, но это механизм для по-настоящему нештатных ситуаций, а не замена try/catch.
Глубже. Идиома: if err != nil { return fmt.Errorf("что делали: %w", err) }, обёртка через %w, проверка через errors.Is/errors.As. panic разматывает стек, выполняя отложенные функции; recover работает только внутри defer и только в той горутине, где произошла паника — панику из другой горутины перехватить нельзя, процесс упадёт. Использовать панику как поток управления допустимо только внутри пакета (пример из стандартной библиотеки — парсер encoding/json ранних версий, text/template), а наружу отдавать error. Есть категории ошибок, которые recover не ловит вовсе: fatal error: concurrent map writes, deadlock всех горутин, переполнение стека.
С какими языками работал кроме Go?
Заголовок раздела «С какими языками работал кроме Go?»Коротко. Вопрос про личный опыт. Интервьюер проверяет широту кругозора и способность сравнивать модели: он хочет услышать 2–3 языка с указанием, что именно вы на них делали и чем они отличаются по модели памяти/конкурентности.
Глубже. Каркас ответа: (1) назвать язык и контекст — «Python: ETL и сервисы на FastAPI, 3 года»; (2) сказать, что перенесли в Go из опыта — например, привычка к типизации из TypeScript или понимание GIL из Python; (3) обозначить, где чувствуете себя слабее, честно. Типичные ошибки: перечислить десять языков «на уровне hello world»; ругать прошлые языки («PHP — говно») — интервьюер услышит, как вы будете говорить о его стеке; не иметь ни одного конкретного сравнения (например, «в Python конкурентность через asyncio — кооперативная в одном потоке, в Go горутины реально раскладываются на ядра»).
Как реализована многозадачность в ос и в Go?
Заголовок раздела «Как реализована многозадачность в ос и в Go?»Коротко. В ОС — вытесняющая многозадачность: планировщик ядра раздаёт потокам кванты времени и переключает их по таймерному прерыванию, переключение контекста стоит порядка микросекунд и требует перехода в ядро. В Go поверх этого лежит собственный планировщик M:N: множество горутин мультиплексируется на небольшое число потоков ОС по модели GMP.
Глубже. G — горутина (стек от 8 КБ, растёт копированием), M — поток ОС, P — логический процессор, «право исполнять Go-код»; число P равно GOMAXPROCS. У каждого P своя локальная очередь runnable-горутин (256 слотов) плюс глобальная очередь; простаивающий P крадёт работу у соседей (work stealing). Когда горутина уходит в блокирующий системный вызов, M блокируется вместе с ней, но P отцепляется и отдаётся другому M — поэтому блокирующий read не останавливает остальные горутины. Сетевой ввод-вывод не блокирует поток вовсе: он идёт через netpoller (epoll/kqueue/IOCP), горутина паркуется и будится по готовности fd. До Go 1.14 вытеснение было кооперативным (только в точках вызова функций), с Go 1.14 добавлено асинхронное вытеснение сигналом SIGURG, поэтому «горячий цикл без вызовов» больше не вешает планировщик. Фоновый поток sysmon отбирает P у горутин, работающих дольше ~10 мс, и будит netpoller.
Примитивные типы в Go. Для чего нужны?
Заголовок раздела «Примитивные типы в Go. Для чего нужны?»Коротко. Примитивные (предопределённые) типы — булев bool, числовые (int, int8/16/32/64, uint и его размерности, uintptr, float32/64, complex64/128), string, а также псевдонимы byte = uint8 и rune = int32. Это кирпичики, из которых строятся составные типы; они хранятся по значению и сравниваются оператором ==.
Глубже. Нужны, чтобы задать точный размер и семантику данных: int32/int64 — когда важен формат (протокол, БД, файл), uint8/byte — для бинарных данных, rune — для кодовой точки Unicode при обходе строки, uintptr — для арифметики с адресами (не удерживает объект от GC!). Все они имеют определённое нулевое значение и никогда не равны nil. Целые со знаком при переполнении заворачиваются по модулю 2^n (это определено спецификацией, не UB, как в C); беззнаковые — тоже. Константы в Go нетипизированные и произвольной точности, тип получают в момент использования.
Разница между int и int64.
Заголовок раздела «Разница между int и int64.»Коротко. int — платформенно-зависимый тип: 64 бита на amd64/arm64 и 32 бита на 386/arm/wasm. int64 всегда ровно 64 бита. Это разные типы, даже когда размеры совпадают, — присваивать и складывать их без явного преобразования нельзя.
Глубже. int — тип по умолчанию для целых литералов, индексов, len(), cap(); его и надо использовать в обычном коде. int64 берут там, где размер обязан быть фиксированным: сериализация, идентификаторы из БД, time.Duration (это int64), счётчики, которые не должны переполниться на 32-битной платформе. Смешивать надо явно, помня о риске потери данных при сужении:
var a int = 5var b int64 = 10// c := a + b // ошибка компиляции: mismatched types int and int64c := int64(a) + b // окd := int(b) // на 32-битной платформе значение может обрезаться_ = c_ = dРазмер int в рантайме — strconv.IntSize или bits.UintSize.
Что такое асинхронность в Go?
Заголовок раздела «Что такое асинхронность в Go?»Коротко. В Go нет async/await: вместо явной асинхронности код пишется в синхронном стиле, а «асинхронность» обеспечивает рантайм — блокирующий с виду вызов паркует горутину, а поток ОС уходит выполнять другие горутины. Запуск асинхронной работы — это go f(), а ожидание результата — канал, sync.WaitGroup или errgroup.
Глубже. Технически это M:N-планирование плюс netpoller: conn.Read выглядит как блокирующий вызов, но под капотом дескриптор в неблокирующем режиме зарегистрирован в epoll, и горутина снимается с потока до готовности данных. Отсюда отсутствие «раскраски функций» (function coloring), которой страдают async/await-языки. Практические следствия: не нужно бояться писать линейный код; но нужно всегда иметь способ остановить асинхронную работу (context.Context) и дождаться её (иначе горутины утекают), а также помнить, что go не даёт никаких гарантий порядка запуска.
g, ctx := errgroup.WithContext(ctx)for _, u := range urls { g.Go(func() error { return fetch(ctx, u) })}if err := g.Wait(); err != nil { /* ... */ }Сколько куч в Go?
Заголовок раздела «Сколько куч в Go?»Коротко. Одна. У процесса Go ровно одна общая управляемая куча (runtime.mheap), общая для всех горутин; стеки горутин — это не кучи, а отдельные растущие сегменты.
Глубже. Иллюзия «нескольких куч» возникает из-за трёхуровневого аллокатора: mcache — локальный кэш аллокаций у каждого P (позволяет выделять маленькие объекты без блокировок), mcentral — общий на класс размера, mheap — глобальная куча, которая берёт память у ОС аренами по 64 МБ и нарезает их на spans по size classes (~68 классов). Это уровни одной кучи, а не разные кучи. Отдельно от неё живут: стеки горутин (начинаются с 8 КБ, растут копированием, лимит по умолчанию 1 ГБ на 64-битных, debug.SetMaxStack), внутренние структуры рантайма, и — если используется cgo — куча C (malloc), которую GC не видит и не собирает. Поколенческих (young/old) куч в Go нет: сборщик неперемещающий, конкурентный, tri-color mark-and-sweep.
Как устроено пустое значение в парадигме Go?
Заголовок раздела «Как устроено пустое значение в парадигме Go?»Коротко. В Go нет null/undefined отдельно от системы типов: у каждого типа есть zero value, и любая переменная без явного инициализатора получает именно его. 0, false, "", nil, покомпонентный ноль для массивов и структур.
Глубже. Дизайн-принцип «make the zero value useful»: var mu sync.Mutex, var wg sync.WaitGroup, var buf bytes.Buffer, var m map[string]int (читать можно), var s []int (append работает) — всё используется без конструктора. Память под новые объекты рантайм всегда зануляет, поэтому «мусора» из прошлых значений не бывает. Отдельная сущность — struct{}, тип с ровно одним значением и нулевым размером: используется в map[T]struct{} и chan struct{} как «сигнал без данных». Главная ловушка — nil-интерфейс: интерфейсное значение хранит пару (тип, данные) и равно nil, только если оба поля нулевые, поэтому *MyErr(nil), положенный в error, даёт err != nil.
type MyErr struct{ Code int }
func (e *MyErr) Error() string { return "boom" }
func bad() error { var e *MyErr = nil return e // интерфейс с типом *MyErr и nil-данными}
// fmt.Println(bad() == nil) // false — классическая ошибкаКакое максимальное количество значений может возвращать функция в Go?
Заголовок раздела «Какое максимальное количество значений может возвращать функция в Go?»Коротко. Спецификация языка жёсткого предела не задаёт — возвращать можно сколько угодно значений. Практический предел ставит компилятор (размер кадра стека) и здравый смысл: больше трёх результатов — сигнал завести структуру.
Глубже. Множественные возвраты — это не кортеж, а именно несколько результатов; их нельзя присвоить одной переменной и нельзя передать частично. Особые правила: результат вызова функции с N результатами можно целиком передать как аргументы в другую функцию, ожидающую ровно эти N параметров (f(g())), и нельзя смешивать с другими аргументами. Идиома — последний результат error, второй результат bool в comma-ok формах (v, ok := m[k], v, ok := <-ch, v, ok := i.(T)). Именованные результаты полезны для документирования и для правки значения в defer, но злоупотребление ими (naked return) ухудшает читаемость.
Если внутри цикла находится switch , как с помощью break выйти именно из внешнего цикла, а не из switch ?
Заголовок раздела «Если внутри цикла находится switch , как с помощью break выйти именно из внешнего цикла, а не из switch ?»Коротко. Пометить цикл меткой и написать break Label. Голый break внутри switch завершает только switch.
Глубже. То же самое работает для select и для вложенных циклов; continue Label продолжает помеченный цикл. Метка должна стоять непосредственно перед оператором цикла. Альтернатива — флаг или вынос куска в функцию с return, но метка читается лучше.
loop: for _, v := range vals { switch v { case "stop": break loop // выходим из for case "skip": continue loop default: fmt.Println(v) } }Как тебе после питона Go?
Заголовок раздела «Как тебе после питона Go?»Коротко. Вопрос про личный опыт и мотивацию. Интервьюер хочет понять, осознанно ли вы перешли и видите ли реальные различия, а не «Go быстрый, Python медленный».
Глубже. Каркас ответа: (1) что понравилось конкретно — статическая типизация ловит ошибки на компиляции, один бинарник вместо venv/докер-слоёв, встроенные горутины вместо asyncio с раскраской функций, единый gofmt и отсутствие споров о стиле, стандартная библиотека с http-сервером из коробки; (2) что было непривычно — многословная обработка ошибок вместо исключений, отсутствие списковых включений и богатых итераторов (частично закрыто slices/maps с 1.21 и range-over-func с 1.23), дженерики появились поздно и ограничены, нет привычного магического ORM; (3) вывод: где выбрал бы Python и сегодня (ML, скрипты, data-обработка), где Go (сетевые сервисы, CLI, высокая конкурентность). Ошибки: отвечать одной фразой «нормально»; хвалить Go абстрактно; жаловаться на if err != nil без понимания, зачем так сделано.
Какие знаешь типы в Go?
Заголовок раздела «Какие знаешь типы в Go?»Коротко. Базовые: bool, числовые (int/int8..64, uint/uint8..64, uintptr, float32/64, complex64/128), string. Составные: массив, слайс, структура, указатель, функция, интерфейс, мапа, канал. Плюс псевдонимы byte и rune и специальный unsafe.Pointer.
Глубже. Полезно уметь разложить их по осям: (1) агрегатные vs ссылочные — массив и структура копируются целиком, слайс/мапа/канал копируют дескриптор; (2) сравнимые (comparable) vs нет — сравнивать == нельзя слайсы, мапы и функции, а структуры и массивы сравнимы, если сравнимы все их элементы (это же определяет, что можно класть ключом в мапу); (3) nilable vs нет (см. схему в начале). Дополнительно: type MyInt int создаёт новый именованный тип (с ним нужна явная конверсия), а type MyInt = int — псевдоним (полностью взаимозаменяем). С Go 1.18 есть параметризованные типы (дженерики) и интерфейсы с наборами типов (~int | ~string), которые нельзя использовать как обычные интерфейсные значения.
В чем разница когда мы пишем int() и int32, int64 и т.д?
Заголовок раздела «В чем разница когда мы пишем int() и int32, int64 и т.д?»Коротко. int32, int64 — это имена типов (используются в объявлениях), а int64(x) — конверсия типа: синтаксис, похожий на вызов функции, который преобразует значение x к типу int64. Функции int() в языке нет.
Глубже. Конверсия между числовыми типами всегда явная, и она может терять данные: сужение обрезает старшие биты (int32(int64(1<<40)) даст 0), int(2.9) отбрасывает дробную часть (усечение к нулю, не округление), а конверсия отрицательного числа в беззнаковый тип даёт значение по модулю 2^n. Отдельно стоят конверсии, которые не про числа: string(65) даёт "A" (кодовая точка, go vet предупреждает), а string(byteSlice) и []byte(s), []rune(s) копируют данные. Конверсия константы проверяется на этапе компиляции: int8(300) не соберётся.
var x int64 = 1 << 40fmt.Println(int32(x)) // 0 — обрезали старшие биты
var f float64 = 2.9fmt.Println(int(f)) // 2 — усечение к нулю, а не округление
var n int = -1fmt.Println(uint8(n)) // 255 — по модулю 2^8// fmt.Println(uint8(-1)) // ошибка компиляции: константа -1 не влезает в uint8// fmt.Println(int(2.9)) // ошибка компиляции: константа 2.9 не целаяКак бы ты реализовал set на Go?
Заголовок раздела «Как бы ты реализовал set на Go?»Коротко. map[T]struct{} для минимума памяти, либо тонкая обёртка с методами Add/Has/Delete/Len. Для конкурентного доступа — обернуть мапу в sync.RWMutex.
Глубже. С дженериками (Go 1.18+) получается универсальный вариант; ограничение comparable обязательно, потому что ключ мапы должен быть сравнимым.
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](items ...T) Set[T] { s := make(Set[T], len(items)) for _, it := range items { s[it] = struct{}{} } return s}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }func (s Set[T]) Delete(v T) { delete(s, v) }func (s Set[T]) Len() int { return len(s) }О чём стоит сказать вслух: порядок обхода не определён (мапа рандомизирует range); методы на map-типе можно объявлять с value-получателем, потому что мапа — указатель на заголовок, но Add на nil-множестве запаникует, поэтому конструктор обязателен; если нужны отсортированный вывод или операции объединения/пересечения — они пишутся тривиально через range, а сортировка через slices.Sorted(maps.Keys(s)) (Go 1.23+).
Какие сервисы вы разрабатывали на Go? За какие функции они отвечали и как взаимодействовали друг с другом?
Заголовок раздела «Какие сервисы вы разрабатывали на Go? За какие функции они отвечали и как взаимодействовали друг с другом?»Коротко. Вопрос про личный опыт. Интервьюер проверяет, понимаете ли вы систему целиком, а не только свой хендлер: границы сервиса, протоколы, данные, нагрузку.
Глубже. Каркас ответа на 2–3 минуты: (1) домен и место сервиса — «сервис биллинга: считает списания за подписки»; (2) цифры — RPS, объём данных, SLA, размер команды; (3) интерфейсы — «наружу gRPC, для фронта REST-гейтвей, асинхронно публикуем события в Kafka, читаем из Postgres, кеш в Redis»; (4) как взаимодействуют — синхронно/асинхронно, идемпотентность, ретраи, таймауты, что происходит при отказе соседа; (5) ваш личный вклад — что именно спроектировали и что бы переделали сейчас. Типичные ошибки: рассказывать про технологии вместо задач («мы использовали gRPC и Kafka»), не знать нагрузочных цифр, говорить «мы» без уточнения своей роли, не уметь нарисовать схему на доске.
Как вы оцениваете свой текущий уровень как разработчика на Go?
Заголовок раздела «Как вы оцениваете свой текущий уровень как разработчика на Go?»Коротко. Вопрос про самооценку. Интервьюер проверяет калибровку: совпадает ли ваша оценка с тем, что он увидит на техническом этапе, и умеете ли вы говорить о своих пробелах.
Глубже. Хороший ответ строится из трёх частей: (1) конкретная привязка — «уверенный middle, ближе к senior в бэкенд-сервисах»; (2) на чём основано — «самостоятельно проектирую сервисы, разбираюсь в рантайме до уровня GMP и escape analysis, профилирую pprof, менторю джунов»; (3) честно названные зоны роста — «слабее в низкоуровневых оптимизациях и в Kubernetes-операторах, сейчас закрываю». Ошибки: «я эксперт» без доказательств (проверят и разочаруются); «я junior» при пяти годах опыта (продадитесь дешевле и вызовете сомнения); уход от ответа. Уровень называйте под конкретную вакансию, а не абстрактно.
Какие примитивы синхронизации существуют в Go?
Заголовок раздела «Какие примитивы синхронизации существуют в Go?»Коротко. Три группы: каналы (идиоматичный способ передавать владение данными), пакет sync — Mutex, RWMutex, WaitGroup, Once, Cond, Pool, Map, и пакет sync/atomic — атомарные операции и типы (atomic.Int64, atomic.Pointer[T] и т.д.). Рядом стоят context для отмены и golang.org/x/sync (errgroup, semaphore, singleflight).
Глубже. Что важно знать про каждый: Mutex невозвратный (рекурсивный захват = deadlock) и с Go 1.9 имеет starvation mode — ожидающий дольше 1 мс получает мьютекс вне очереди. RWMutex выгоден только при действительно преобладающем чтении и длинных критических секциях; при высокой конкуренции он проигрывает обычному мьютексу из-за общего счётчика. WaitGroup: Add должен вызываться до go, копировать её нельзя (передавать только указателем), в Go 1.25 добавлен метод WaitGroup.Go, который сам делает Add/Done. Once.Do гарантирует ровно один запуск, даже если функция запаниковала (повтора не будет); в Go 1.21 добавлены sync.OnceFunc, OnceValue, OnceValues. Cond нужен редко, почти всегда заменяется каналом. Pool — кеш временных объектов, очищается при GC, не пригоден как пул соединений. sync.Map оптимизирован под два сценария: ключ пишется один раз и много раз читается, либо разные горутины работают с непересекающимися ключами (в Go 1.24 переписан на HashTrieMap). atomic — единственный корректный способ «просто счётчика»; типизированные обёртки из Go 1.19 предпочтительнее функций, так как исключают ошибку выравнивания на 32-битных платформах. Все они опираются на Go Memory Model, где определено отношение happens-before.
What is the Sync package for?
Заголовок раздела «What is the Sync package for?»Коротко. sync даёт низкоуровневые примитивы для координации горутин, работающих с общей памятью: взаимное исключение (Mutex, RWMutex), ожидание группы (WaitGroup), однократная инициализация (Once), условные переменные (Cond), пул временных объектов (Pool) и конкурентную мапу (sync.Map).
Глубже. См. выше «Какие примитивы синхронизации существуют в Go?» — там детали по каждому типу. Общее правило из документации пакета: значения типов sync нельзя копировать после первого использования (go vet ловит это проверкой copylocks), поэтому их передают указателем или встраивают в структуру, которую тоже передают указателем. Идиоматический выбор между каналом и мьютексом: канал — когда данные передаются от одной горутины к другой; мьютекс — когда состояние остаётся на месте, а доступ к нему разделяется.
What is a consistent hash? What is it for?
Заголовок раздела «What is a consistent hash? What is it for?»Коротко. Консистентное хеширование — способ распределять ключи по N узлам так, чтобы при добавлении или удалении узла переезжало примерно K/N ключей, а не почти все, как при hash(key) % N. Узлы и ключи отображаются на одно кольцо хешей, ключ идёт к первому узлу по часовой стрелке.
Глубже. Чтобы распределение было равномерным, каждый физический узел размещают на кольце в виде десятков-сотен виртуальных узлов (реплик хеша) — иначе разброс нагрузки получается большим. Применяется в распределённых кешах (memcached-клиенты, Redis Cluster использует другой механизм — 16384 слота, что по сути та же идея фиксированных слотов), в шардировании БД, в балансировке (consistent hashing with bounded loads у Envoy/nginx), в Dynamo/Cassandra. Альтернативы, которые стоит назвать: rendezvous hashing (HRW) — считаем hash(key, node) для всех узлов и берём максимум, тоже перемещает K/N ключей и проще в реализации; jump consistent hash (Google) — O(1) памяти, но узлы нельзя удалять из середины. В Go есть готовые библиотеки (stathat/consistent, buraksezer/consistent), а базовая реализация — отсортированный слайс хешей плюс sort.Search для двоичного поиска по кольцу.
Чем метод отличается от функции в Go?
Заголовок раздела «Чем метод отличается от функции в Go?»Коротко. Метод — это функция с получателем (receiver), объявленным до имени: func (u *User) Name() string. Он привязан к именованному типу, входит в его method set и вызывается через значение этого типа; функция самостоятельна и вызывается по имени.
Глубже. Практические различия: (1) метод можно объявить только в том же пакете, где объявлен тип — «дописать» метод к time.Time или к int нельзя, нужен свой именованный тип; (2) выбор value- или pointer-получателя определяет, увидит ли вызывающий изменения и какой method set удовлетворяет интерфейс: у *T в method set входят методы и с T, и с *T, у T — только с T; (3) Go автоматически берёт адрес или разыменовывает при вызове через адресуемое значение (v.PtrMethod() = (&v).PtrMethod()), но неадресуемое значение (элемент мапы, результат вызова) так не работает; (4) метод можно превратить в функцию: method value f := v.Method (получатель захвачен и зафиксирован в момент взятия) и method expression f := T.Method (получатель становится первым параметром). На уровне генерации кода метод — обычная функция с дополнительным первым аргументом; разница в основном в типах и method sets, а не в машинном коде.
Какие минусы видишь в Go?
Заголовок раздела «Какие минусы видишь в Go?»Коротко. Многословная обработка ошибок, ограниченные дженерики, отсутствие суммарных типов (enum/union) и Option, слабая система выражения неизменяемости (нет const для полей/структур), nil как отдельный класс багов, тяжёлая история с зависимостями от cgo, и рантайм с GC, который делает Go неподходящим для hard real-time.
Глубже. Развёрнутый и честный список: (1) if err != nil занимает до трети кода, а попытки исправить это (try, ?) отклонены сообществом; (2) дженерики без методов на параметризованных типах, без вариантности и с ограниченным выводом типов, плюс стоимость через GC shape stenciling; (3) нет алгебраических типов — enum эмулируется iota, и компилятор не проверит полноту switch; (4) nil может быть у шести категорий типов, отсюда nil-интерфейс, nil-мапа при записи, nil-канал; (5) отсутствие иммутабельности и явного владения означает, что защиту от гонок обеспечивает только дисциплина плюс race detector; (6) GC даёт паузы (обычно доли миллисекунды, но stop-the-world фазы есть) и нет ручного контроля памяти, кроме GOGC/GOMEMLIMIT и арен-экспериментов; (7) стандартная библиотека местами устарела по API (net/http до 1.22 без роутинга по методам, time без монотонных API в удобной форме); (8) сборка с cgo ломает кросс-компиляцию и статическую линковку. Важно закончить конструктивно: минусы — цена за простоту, скорость компиляции и низкий порог входа в чужой код.
Вопросы: Много вопросов как устроена ОС под капотом и про многопоточность в Go.
Заголовок раздела «Вопросы: Много вопросов как устроена ОС под капотом и про многопоточность в Go.»Коротко. Обрывок исходника, вопрос не восстанавливается: это описание секции интервью, а не конкретный вопрос.
Глубже. Что реально спрашивают в такой секции: процесс vs поток, виртуальная память и страницы, page fault, context switch и его цена, системные вызовы и переход в ядро, планировщик ОС (CFS/EEVDF), epoll и неблокирующий I/O, сигналы; со стороны Go — модель GMP, GOMAXPROCS, netpoller, вытеснение с Go 1.14, стеки горутин, что происходит при блокирующем syscall. Ответы на ключевые из них — в вопросах «Как реализована многозадачность в ос и в Go?» и «Что такое многопоточность в Go? Как она устроена?» ниже.
Как можно управлять количеством потоков в Go?
Заголовок раздела «Как можно управлять количеством потоков в Go?»Коротко. Напрямую количеством потоков ОС управлять нельзя — это делает рантайм. Управляют параллелизмом через GOMAXPROCS (число логических процессоров P, то есть максимум горутин, одновременно исполняющих Go-код) и верхней границей потоков через debug.SetMaxThreads.
Глубже. GOMAXPROCS задаётся переменной окружения или runtime.GOMAXPROCS(n); с Go 1.5 по умолчанию равен числу ядер, видимых процессу. В контейнерах это долго было проблемой: рантайм видел все ядра хоста, игнорируя cgroup-лимит CPU, — отсюда популярность uber-go/automaxprocs; в Go 1.25 рантайм научился учитывать cgroup CPU limit по умолчанию. Реальное число потоков ОС обычно больше GOMAXPROCS: новый M создаётся под каждый заблокированный в syscall поток, есть отдельные потоки под sysmon и под GC. Жёсткий предел — runtime/debug.SetMaxThreads, по умолчанию 10000; при превышении процесс падает. Число горутин ограничивают не рантаймом, а прикладными средствами: воркер-пул, semaphore.Weighted, буферизованный канал-семафор, errgroup.SetLimit (x/sync). Диагностика: runtime.NumGoroutine(), runtime.NumCPU(), pprof профиль goroutine, GODEBUG=schedtrace=1000.
Назови основные типы данных в Go?
Заголовок раздела «Назови основные типы данных в Go?»Коротко. См. выше «Какие знаешь типы в Go?». Кратко: bool, целые со знаком и без (int, int8..int64, uint, uint8..uint64, uintptr), float32/64, complex64/128, string, и составные — массив, слайс, мапа, структура, указатель, функция, интерфейс, канал.
Глубже. Отличие этой формулировки от предыдущей — обычно вопрос ждёт короткого перечисления «базовых» типов и одного-двух уточнений: что byte и rune — псевдонимы uint8 и int32, и что string — неизменяемая последовательность байтов (заголовок из указателя и длины), а не массив символов.
Есть ли наследования в Go?
Заголовок раздела «Есть ли наследования в Go?»Коротко. Нет. Классов и наследования в Go нет; переиспользование строится на композиции — встраивании (embedding) структур и интерфейсов — и на интерфейсах, которые удовлетворяются структурно, без объявления «implements».
Глубже. Встраивание — это не наследование, а синтаксический сахар над делегированием: поля и методы встроенного типа промотируются на внешний, но динамической диспетчеризации «как в ООП» не возникает — метод встроенного типа, вызывая другой метод, вызовет свой, а не переопределённый во внешнем типе (нет виртуальных вызовов, нет super). Полиморфизм даёт интерфейс: любой тип с нужным набором методов ему удовлетворяет. Встраивание интерфейса в структуру — известный приём для частичной реализации (структура с io.ReadWriter внутри удовлетворяет интерфейсу, но паникует на нереализованных методах); так же делают forward-compatible API вроде grpc.UnimplementedXxxServer.
type Animal struct{ Name string }func (a Animal) Speak() string { return a.Name + " makes a sound" }
type Dog struct { Animal // встраивание, не наследование Breed string}
d := Dog{Animal{"Rex"}, "husky"}fmt.Println(d.Speak()) // "Rex makes a sound" — метод промотированfmt.Println(d.Name) // "Rex" — поле промотированоКак передается значение функции?
Заголовок раздела «Как передается значение функции?»Коротко. В Go всё передаётся только по значению: аргумент копируется. Для указателя копируется адрес (поэтому через него можно менять оригинал), для слайса — заголовок из трёх слов (указатель на массив, len, cap), для мапы и канала — указатель на внутреннюю структуру, для структуры и массива — все байты целиком.
Глубже. Практические следствия: (1) изменение поля структуры внутри функции не видно вызывающему, если передали значение, — нужен *T; (2) append внутри функции может как изменить общий массив (если хватило cap), так и выделить новый (тогда вызывающий изменений не увидит) — поэтому append возвращает слайс; (3) копирование большой структуры стоит денег, но и указатель не бесплатен — он часто заставляет объект уехать в кучу; ориентир: до нескольких машинных слов — по значению, больше — по указателю, но решает бенчмарк; (4) типы с sync.Mutex внутри копировать нельзя вовсе. Отдельно: сама «функция как значение» — это указатель на funcval, то есть тоже копия указателя.
В чем отличие ссылки от указателей?
Заголовок раздела «В чем отличие ссылки от указателей?»Коротко. В Go нет ссылок в смысле C++ (int&) — есть только указатели. Слова «ссылочные типы» про слайсы, мапы и каналы — жаргон: это обычные значения, внутри которых лежит указатель на общие данные.
Глубже. Различия, которые стоит проговорить, сравнивая с C++: ссылка в C++ обязана быть проинициализирована, не может быть переприсвоена и не может быть null; указатель в Go — самостоятельное значение, может быть nil, может переприсваиваться, сравнивается через ==. При этом в Go нет и арифметики указателей (она доступна только через unsafe и uintptr), нет разыменования произвольных адресов и нет висячих указателей: если указатель жив, объект жив, потому что escape analysis отправит его в кучу, а GC не соберёт. Внутри рантайма Go «передача по ссылке» есть только в одном месте — получатель метода с указателем и, формально, range по массиву даёт копию, а по слайсу — нет.
Может ли ссылка быть nil?
Заголовок раздела «Может ли ссылка быть nil?»Коротко. Ссылок в языке нет, а указатель nil быть может — это его нулевое значение. Из «ссылочных типов» nil-значение бывает у слайса, мапы, канала, функции и интерфейса.
Глубже. Что происходит с каждым nil-значением: разыменование nil-указателя даёт панику invalid memory address or nil pointer dereference (это ловится recover), но вызвать метод на nil-указателе можно, если метод не обращается к полям — часто используется для деревьев (func (t *Tree) Sum() int { if t == nil { return 0 } ... }). Nil-слайс: len, cap, range, append работают, индексирование паникует. Nil-мапа: чтение, len, delete, range работают, запись паникует. Nil-канал: чтение и запись блокируются навсегда, close паникует — это используется для отключения ветки в select. Nil-функция: вызов паникует. Nil-интерфейс: вызов метода паникует, а сравнение с nil даёт true только когда и тип, и значение пустые.
Значение каких типов никогда не может быть nil в Go?
Заголовок раздела «Значение каких типов никогда не может быть nil в Go?»Коротко. Всех невырожденных «значимых» типов: bool, все числовые (int*, uint*, float*, complex*, uintptr), string, массивы [N]T и структуры. У них есть нулевое значение, но оно не nil.
Глубже. Nil-абельны ровно семь категорий: указатель, слайс, мапа, канал, функция, интерфейс и unsafe.Pointer. Тонкости: string не может быть nil (пустая строка "" — это не nil, и в отличие от []byte сравнивать её с nil нельзя — ошибка компиляции); uintptr — числовой тип, он равен 0, а не nil, и не удерживает объект от сборки; структура, содержащая nil-поля, сама не nil; массив [3]*int не nil, хотя все его элементы nil. Практический вывод для API: если нужно различать «нет значения» и «нулевое значение» для int/string/bool, приходится использовать указатель, sql.NullXxx или пару (value, ok).
Что такое указатель в Go?
Заголовок раздела «Что такое указатель в Go?»Коротко. Указатель — значение, хранящее адрес другой переменной. Тип записывается *T, адрес берётся &x, разыменование *p; нулевое значение — nil.
Глубже. Отличия от C: арифметики указателей нет (только через unsafe), приведение произвольного uintptr обратно в указатель небезопасно и не гарантируется GC, а сам GC отслеживает указатели, поэтому «висячих» не бывает. Указатели используют, чтобы (1) дать функции или методу изменять оригинал, (2) избежать копирования большой структуры, (3) выразить «значение может отсутствовать». new(T) возвращает *T с нулевым значением; &Struct{...} — идиоматичнее. Указатели сравнимы (==), их можно класть ключом в мапу. Есть ещё unsafe.Pointer — нетипизированный указатель, через который делают конверсии между несовместимыми типами, и uintptr — целое, которое не считается указателем для GC.
x := 42p := &x*p = 43fmt.Println(x) // 43var q *intfmt.Println(q == nil) // true// fmt.Println(*q) // panic: nil pointer dereferenceВыполняет функции сборщика мусора в Go;
Заголовок раздела «Выполняет функции сборщика мусора в Go;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Судя по соседним пунктам, это неверный вариант ответа на вопрос «что такое указатель в Go?». Указатель функций сборщика мусора не выполняет — GC в Go реализован в рантайме (конкурентный неперемещающий mark-and-sweep с write barrier), а указатели он лишь обходит при маркировке, отсюда правило: uintptr объект от сборки не удерживает, а unsafe.Pointer — удерживает.
Встроенный тип данных;
Заголовок раздела «Встроенный тип данных;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Как вариант ответа на «что такое указатель» это формально почти верно: *T — встроенная конструкция типа, но правильный ответ теста — «переменная, которая хранит адрес другой переменной». Предопределённых (встроенных) имён типов в Go 20 штук плюс any и comparable; список — в builtin пакете документации.
Имя функции;
Заголовок раздела «Имя функции;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Как вариант ответа на «что такое указатель» — неверный. Но по теме стоит помнить: имя функции без скобок само является значением типа func(...), и его можно передать (см. вопрос про передачу функции параметром), а получить имя функции в рантайме можно через runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name().
Переменная, которая хранит адрес другой переменной;
Заголовок раздела «Переменная, которая хранит адрес другой переменной;»Коротко. Это правильный вариант ответа на вопрос «что такое указатель в Go?».
Глубже. Развёрнуто — см. «Что такое указатель в Go?» выше. На собеседовании к этой формулировке полезно добавить: размер указателя равен размеру машинного слова (8 байт на 64-битных платформах), нулевое значение — nil, и Go, в отличие от C, гарантирует, что пока указатель достижим, объект жив.
Метка для перехода;
Заголовок раздела «Метка для перехода;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Как ответ про указатель — неверный: метка (label) — это отдельная синтаксическая сущность для break Label, continue Label и goto Label, она не значение и не тип. Пример использования метки — в вопросе про break из внешнего цикла выше.
Какой из следующих пакетов подходит для работы с нетипизированными указателями в Go?
Заголовок раздела «Какой из следующих пакетов подходит для работы с нетипизированными указателями в Go?»Коротко. Пакет unsafe — он даёт тип unsafe.Pointer (нетипизированный указатель, к которому можно привести любой *T) и функции Sizeof, Alignof, Offsetof, а с Go 1.17 — Add и Slice, с Go 1.20 — SliceData, String, StringData.
Глубже. Правила «unsafe.Pointer» описаны в документации пакета шестью допустимыми паттернами; главное: uintptr нельзя хранить между операциями, потому что GC его не видит, а стек горутины может переехать при росте. Классические применения: zero-copy конверсия []byte ↔ string (unsafe.String/unsafe.StringData), работа со структурами из C через cgo, реализация низкоуровневых контейнеров. Рядом стоят reflect (типизированная рефлексия, reflect.Value.UnsafePointer) и runtime — но нетипизированные указатели именно в unsafe. Код с unsafe не проверяется на безопасность типов и может сломаться при смене версии Go.
pointers ;
Заголовок раздела «pointers ;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Такого пакета в стандартной библиотеке нет — это дистрактор для предыдущего вопроса; правильный ответ unsafe.
Указатели;
Заголовок раздела «Указатели;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. По контексту это вариант ответа на вопрос «какой тип является ссылочным». Указатель действительно относят к «ссылочным» типам в широком смысле, но в терминологии Go «ссылочными» обычно называют слайсы, мапы и каналы; в спецификации термина reference type нет вовсе.
Булевые типы;
Заголовок раздела «Булевые типы;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. bool — предопределённый тип с двумя значениями true/false, нулевое значение false, размер 1 байт. Числовые конверсии к нему запрещены: int(true) и bool(1) не компилируются, а конструкции вроде if x при нелогическом x — тоже ошибка.
Числовые типы;
Заголовок раздела «Числовые типы;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Как элемент классификации: числовые типы Go — целые со знаком (int8/16/32/64, int), беззнаковые (uint8/16/32/64, uint, uintptr), с плавающей точкой (float32/64, IEEE-754) и комплексные (complex64/128). Nil у них не бывает, нулевое значение — 0.
Какой из перечисленных типов данных является ссылочным в Go?
Заголовок раздела «Какой из перечисленных типов данных является ссылочным в Go?»Коротко. Из типичного набора вариантов — слайс, мапа и канал (плюс указатель и функция, если они есть в списке). bool, числа, string, массивы и структуры — значимые.
Глубже. Строго говоря, спецификация Go термина «reference type» не использует: все типы передаются по значению. «Ссылочность» слайса, мапы и канала означает, что копируется дескриптор, а данные общие. Разница между ними важна: у слайса копируется заголовок целиком, поэтому изменение элемента видно всем копиям, а append — не обязательно; у мапы и канала копируется один указатель, поэтому видно всё. Строка формально тоже содержит указатель, но она неизменяема, поэтому «ссылочной» её не называют.
Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Как ответ на «какой тип ссылочный» — неверный: bool значимый, копируется целиком и никогда не бывает nil.
x = int ;
Заголовок раздела «x = int ;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Синтаксически это недопустимая конструкция: int — имя типа, его нельзя присвоить переменной (ошибка компиляции type int is not an expression). Похоже, это дистрактор к вопросу «как правильно объявить переменную»; корректные формы — var x int, x := 0, var x = 0, x := int(0). Единственное место, где имя типа стоит справа от =, — объявление псевдонима: type MyInt = int.
Какие ключевые слова используются для управления потоком выполнения в Go?
Заголовок раздела «Какие ключевые слова используются для управления потоком выполнения в Go?»Коротко. if, else, for, range, switch, case, default, fallthrough, select, break, continue, goto, return, defer. Отдельного while и do..while в Go нет — их роль играет for.
Глубже. Всего в Go 25 ключевых слов, и это сознательное ограничение. Особенности: for имеет четыре формы (классическая с тремя частями, с одним условием как while, бесконечная и range; c Go 1.22 работает for i := range 10, а с Go 1.23 — range-over-func по итераторам); switch не проваливается между ветками по умолчанию, для проваливания есть fallthrough; существует switch без выражения (switch { case x > 0: }) и type switch (switch v := i.(type)); select мультиплексирует операции с каналами и при нескольких готовых выбирает псевдослучайно; defer откладывает вызов до выхода из функции (не из блока), аргументы вычисляются в момент объявления. goto есть, но не может перепрыгивать через объявления переменных и входить внутрь блока.
if , else ;
Заголовок раздела «if , else ;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Верный вариант к предыдущему вопросу. Особенность Go: у if есть форма с инициализатором if v, ok := m[k]; ok { ... }, переменная видна и в else; скобок вокруг условия нет, фигурные скобки обязательны, а else обязан стоять на строке с закрывающей скобкой из-за автоматической вставки точек с запятой.
Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Верный вариант. Единственный цикл в языке; формы перечислены выше. Важные детали для собеседования: range по массиву/слайсу копирует элемент в переменную (для больших структур используйте индекс), range по мапе идёт в случайном порядке, range по строке идёт по рунам с декодированием UTF-8, range по каналу читает до close. С Go 1.22 переменные цикла — новые на каждой итерации.
switch , case ;
Заголовок раздела «switch , case ;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Верный вариант. Кроме обычного switch есть type switch, и в нём case nil ловит нулевой интерфейс, а case с несколькими типами оставляет v типа исходного интерфейса. Вложенный switch внутри цикла — источник вопроса про break Label выше.
Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Ключевого слова loop в Go нет — это дистрактор. Слово loop встречается только как имя метки (как в примере loop: выше), и это обычный идентификатор.
Как можно проверить, равны ли два среза в Go?
Заголовок раздела «Как можно проверить, равны ли два среза в Go?»Коротко. Оператором == нельзя — слайсы сравнимы только с nil. Идиоматичный способ с Go 1.21 — slices.Equal(a, b) (и slices.EqualFunc для своего компаратора); для []byte — bytes.Equal; универсально, но медленно — reflect.DeepEqual.
Глубже. Почему нет ==: сравнение слайсов пришлось бы определять либо как поверхностное (одинаковый указатель), либо как глубокое — оба варианта неочевидны, и авторы отказались от выбора. Отличия способов: slices.Equal сравнивает поэлементно оператором ==, требует comparable-элементы, считает nil и пустой слайс равными и работает без аллокаций; reflect.DeepEqual идёт по рефлексии (медленно, обычно в 10–100 раз), различает nil и пустой слайс, зато умеет вложенные структуры и мапы; bytes.Equal для байтов быстрее всех (векторизован в ассемблере). Для отсортированности/порядка есть slices.Compare. В тестах удобен github.com/google/go-cmp/cmp.Diff.
a := []int{1, 2, 3}b := []int{1, 2, 3}fmt.Println(slices.Equal(a, b)) // truefmt.Println(reflect.DeepEqual(a, b)) // truefmt.Println(slices.Equal([]int(nil), []int{})) // true, а DeepEqual даст falsevar x int .
Заголовок раздела «var x int .»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Это каноничное объявление переменной с явным типом и нулевым значением: x == 0. Полезно помнить отличия форм: var x int — работает и на уровне пакета, и внутри функции; x := 0 — только внутри функции и требует хотя бы одной новой переменной слева; var x = 0 — тип выводится (int); неиспользуемая локальная переменная — ошибка компиляции, а неиспользуемая переменная уровня пакета — нет.
Где был разработан язык Go?
Заголовок раздела «Где был разработан язык Go?»Коротко. В Google. Проект начали в 2007 году Роберт Гризмер, Роб Пайк и Кен Томпсон; язык публично анонсировали в ноябре 2009, версия 1.0 вышла в марте 2012.
Глубже. Мотивация — боль внутренней разработки в Google: очень долгая сборка больших C++-проектов, сложность языков и плохая поддержка многоядерности и сетевых сервисов. Отсюда дизайн: быстрая компиляция (никаких заголовков, явный граф импортов без циклов), простая грамматика, встроенная конкурентность на идеях CSP Хоара, сборка мусора, статическая линковка. Кен Томпсон — соавтор Unix и языка B, Роб Пайк — Plan 9 и UTF-8, что видно в стандартной библиотеке. С 2015 года компилятор целиком написан на Go (раньше был на C), а проект развивается открыто под лицензией BSD; сейчас язык курирует Go team в Google плюс внешние контрибьюторы, изменения проходят через процесс proposal.
Зарезервированные функции;
Заголовок раздела «Зарезервированные функции;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. По смыслу близко к встроенным (builtin) функциям Go, которых немного и они не принадлежат ни одному пакету: len, cap, new, make, append, copy, delete, close, panic, recover, print, println, complex, real, imag, min/max (Go 1.21), clear (Go 1.21). Это не ключевые слова: их имена можно затенить своей переменной (len := 5), после чего builtin станет недоступен.
Тип данных;
Заголовок раздела «Тип данных;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Как вариант ответа в тесте встречается к вопросам вида «что такое map/chan/error». error — предопределённый интерфейс с единственным методом Error() string; map и chan — ключевые слова, вводящие составные типы; any — псевдоним interface{} с Go 1.18.
Специальное имя пространства имен;
Заголовок раздела «Специальное имя пространства имен;»Коротко. Обрывок исходника (вариант ответа в тесте), вопрос не восстанавливается.
Глубже. Пространств имён в смысле C++/C# в Go нет: единица инкапсуляции — пакет, а видимость определяется регистром первой буквы идентификатора (экспортируется — с заглавной). Специальные имена, которые бывают в вариантах таких тестов: main (пакет и функция точки входа), init (функция инициализации, может быть несколько в пакете, вызывается до main), _ (blank identifier), internal/ (каталог с ограниченной видимостью), vendor/.
Какие из этих структур, используемых для взаимодействия с ОС, существуют в пакете os в Go?
Заголовок раздела «Какие из этих структур, используемых для взаимодействия с ОС, существуют в пакете os в Go?»Коротко. Основные типы os: os.File, os.Process, os.ProcessState, os.ProcAttr, os.DirEntry и os.FileInfo (псевдонимы fs.DirEntry/fs.FileInfo), os.FileMode, а также типы ошибок os.PathError (= fs.PathError), os.LinkError, os.SyscallError и интерфейс os.Signal. Плюс готовые переменные os.Stdin, os.Stdout, os.Stderr, os.Args.
Глубже. Пакет os — платформенно-независимая обёртка над системными вызовами; всё, что не покрывается, лежит в syscall (заморожен) и golang.org/x/sys/unix. Полезно знать: os.File реализует io.Reader/io.Writer/io.Closer и io.ReaderAt; работа с процессами — os/exec (exec.Cmd), а не os; сигналы — os/signal (signal.Notify, signal.NotifyContext с Go 1.16); переменные окружения — os.Getenv/LookupEnv/Environ; с Go 1.16 в os переехали os.ReadFile, os.WriteFile, os.ReadDir из io/ioutil (устарел), появился os.DirFS, а с Go 1.24 — os.Root для операций в пределах каталога без выхода наружу по симлинкам.
Задача исправить некорректный многопоточный код на Go;
Заголовок раздела «Задача исправить некорректный многопоточный код на Go;»Коротко. Обрывок исходника (вопрос не восстанавливается дословно), но задача типовая: найти гонку данных и исправить её мьютексом, атомиком или каналом. Первое, что стоит сказать вслух, — «запустил бы с go test -race».
Глубже. Топ дефектов, которые дают в таких задачах: (1) незащищённый инкремент общей переменной из горутин — лечится sync/atomic или sync.Mutex; (2) sync.WaitGroup передана в функцию по значению — копия, Wait зависнет; передавать только *sync.WaitGroup; (3) wg.Add(1) вызван внутри горутины — гонка с Wait; (4) захват переменной цикла (актуально до Go 1.22); (5) конкурентная запись в мапу — fatal error: concurrent map writes, не ловится recover; (6) запись в закрытый канал или двойной close — паника, закрывать должен только отправитель; (7) утечка горутин: никто не читает из небуферизованного канала, нет context для отмены; (8) defer mu.Unlock() забыт при раннем return.
// было: гонкаvar counter intfor i := 0; i < 100; i++ { go func() { counter++ }()}
// сталоvar counter atomic.Int64var wg sync.WaitGroupfor i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() counter.Add(1) }()}wg.Wait()fmt.Println(counter.Load()) // 100В чем плюсы и минусы Go?
Заголовок раздела «В чем плюсы и минусы Go?»Коротко. Плюсы: простота и быстрое чтение чужого кода, скорость компиляции, один статический бинарник, горутины и каналы из коробки, сильная стандартная библиотека, отличный тулинг (go test, pprof, race detector, gofmt), гарантия обратной совместимости Go 1. Минусы: многословные ошибки, ограниченные дженерики, отсутствие суммарных типов, nil во многих обличьях, GC вместо ручного управления памятью.
Глубже. Развёрнутый разбор минусов — в вопросе «Какие минусы видишь в Go?». Про плюсы стоит уметь сказать не лозунгами, а последствиями: маленькая грамматика означает, что новый человек читает продовый код на второй день; gofmt снимает споры о стиле на код-ревью; статическая линковка даёт образы из scratch в единицы мегабайт; race detector реально ловит гонки в CI; Go 1 compatibility promise означает, что код десятилетней давности собирается сегодняшним компилятором. Хороший ответ заканчивается указанием ниши: Go оптимален для сетевых сервисов, CLI и инфраструктуры и плох для GUI, числодробилок с ручной оптимизацией памяти и hard real-time.
Базовые вопросы по Go;
Заголовок раздела «Базовые вопросы по Go;»Коротко. Обрывок исходника, вопрос не восстанавливается: пометка о характере секции.
Глубже. Под «базовыми» обычно понимают тот же набор, что и в вопросе «Стандартные вопросы про Go»: типы и нулевые значения, слайсы vs массивы, мапы, строки и руны, интерфейсы, указатели, defer, горутины и каналы, обработка ошибок. Всё это разобрано в этом и соседних конспектах.
Что такое многопоточность в Go? Как она устроена?
Заголовок раздела «Что такое многопоточность в Go? Как она устроена?»Коротко. В Go пишут не в потоках, а в горутинах: это легковесные единицы исполнения рантайма (начальный стек 8 КБ, растёт копированием), которые планировщик мультиплексирует на потоки ОС по модели M:N — GMP. Реальный параллелизм ограничен GOMAXPROCS.
Глубже. См. подробности в вопросе «Как реализована многозадачность в ос и в Go?». Что добавить именно здесь: цена запуска горутины — порядка сотен наносекунд против десятков микросекунд у потока ОС, поэтому нормально держать сотни тысяч горутин. Стек растёт не через guard page, а через проверку в прологе функции (morestack) и копирование стека с корректировкой указателей — поэтому в Go нельзя держать указатели на стековые адреса в uintptr. Синхронизация между горутинами описывается Go Memory Model: отправка в канал happens-before завершения соответствующего приёма, mu.Unlock happens-before последующего mu.Lock, once.Do happens-before возврата любого другого Do. Ключевая формула для собеседования: «конкурентность — про структуру программы, параллелизм — про одновременное исполнение; Go даёт первое, а второе получается при GOMAXPROCS > 1».
Как сравнить два числа float?
Заголовок раздела «Как сравнить два числа float?»Коротко. Не оператором ==. Сравнивают с допуском: math.Abs(a-b) <= eps для абсолютной погрешности или относительно масштаба чисел math.Abs(a-b) <= eps*math.Max(math.Abs(a), math.Abs(b)). Отдельно проверяют NaN: math.IsNaN(x), потому что NaN != NaN.
Глубже. Причина — IEEE-754: 0.1 + 0.2 != 0.3, потому что десятичные дроби не представимы точно в двоичной мантиссе. Выбор эпсилона зависит от масштаба: абсолютный допуск ломается на больших числах (для 1e18 шаг между соседними float64 больше единицы), относительный — на числах около нуля; на практике комбинируют оба или сравнивают по числу ULP через math.Nextafter. Особые значения: +0.0 == -0.0 истинно, но math.Signbit их различает; math.Inf(1) == math.Inf(1) истинно; любое сравнение с NaN (кроме !=) ложно, поэтому sort.Float64s со срезом, содержащим NaN, ведёт себя специфично, а slices.Sort использует упорядочение, где NaN идёт первым. Для денег float использовать нельзя — берите целые в минимальных единицах (копейках) или shopspring/decimal.
const eps = 1e-9a, b := 0.1+0.2, 0.3fmt.Println(a == b) // falsefmt.Println(math.Abs(a-b) <= eps) // truenan := math.NaN()fmt.Println(nan == nan, math.IsNaN(nan)) // false trueКак реализовать enum в Go?
Заголовок раздела «Как реализовать enum в Go?»Коротко. Отдельного enum в языке нет. Идиома — именованный целочисленный тип плюс блок констант с iota, и метод String() для читаемого вывода (обычно генерируемый stringer).
Глубже. Что важно проговорить: компилятор не проверяет полноту switch и не запрещает Status(42) — типобезопасность частичная, поэтому в публичных API добавляют валидацию (func (s Status) Valid() bool). Первый элемент часто делают «нулевым/неизвестным», чтобы zero value не значил валидное состояние. Для JSON/БД реализуют MarshalJSON/UnmarshalJSON или driver.Valuer/sql.Scanner. Альтернативы: строковые константы (проще для логов и JSON, но занимают больше), структуры с приватным полем (полная герметичность, но нельзя использовать в switch по значению), и битовые флаги через 1 << iota. Проверку полноты switch можно повесить на линтер exhaustive в golangci-lint.
type Status int
const ( StatusUnknown Status = iota StatusPending StatusActive StatusClosed)
//go:generate stringer -type=Statusfunc (s Status) String() string { switch s { case StatusPending: return "pending" case StatusActive: return "active" case StatusClosed: return "closed" default: return "unknown" }}
// битовые флагиtype Perm uint8
const ( PermRead Perm = 1 << iota PermWrite PermExec)Какие есть ссылочные типы в Go?
Заголовок раздела «Какие есть ссылочные типы в Go?»Коротко. Формально в современной спецификации Go термина «ссылочный тип» нет, и передача всегда идёт по значению. На собеседовании к «ссылочным» обычно относят те типы, чьё значение содержит внутри указатель на общие данные: slice, map, chan, func (замыкание), interface и, конечно, сам *T.
Глубже. Разница между «ссылочными» видна на том, что именно копируется. У мапы и канала копируется один указатель — поэтому любые изменения содержимого видны всем владельцам копии. У слайса копируются три слова: изменение s[i] видно снаружи, а append, вызвавший реаллокацию, — уже нет, потому что новый указатель остался в локальной копии заголовка. У строки копируются два слова (ptr, len), но данные иммутабельны, поэтому «ссылочность» незаметна. Спецификация Go раньше (до 2013 года) действительно использовала слова «reference types» для map/slice/chan; сейчас этот термин удалён именно из-за путаницы. Корректная формулировка на собесе: «В Go нет передачи по ссылке. Есть типы, значение которых — дескриптор с указателем внутрь кучи».
func mutate(s []int, m map[string]int) { s[0] = 42 // видно вызывающему s = append(s, 1) // скорее всего НЕ видно: меняется локальная копия заголовка m["k"] = 1 // видно вызывающему}Что такое offset pointer?
Заголовок раздела «Что такое offset pointer?»Коротко. В спецификации Go такого термина нет. Обычно под этим понимают одно из двух: адресацию «база + смещение» (получение указателя на поле структуры или элемент массива, где адрес считается как адрес объекта плюс unsafe.Offsetof), либо внутренний (interior) указатель — указатель, направленный не на начало объекта, а внутрь него.
Глубже. Практически важны два следствия. Первое: Go-рантайм и GC корректно работают с внутренними указателями — если жив указатель на поле структуры или на элемент массива, живым считается весь объект целиком, поэтому p := &bigStruct.Field удерживает в памяти всю структуру. Второе: арифметика указателей запрещена в обычном Go и возможна только через unsafe, причём по строго описанным правилам — конвертация uintptr → unsafe.Pointer валидна только в одном выражении, иначе GC может передвинуть стек или собрать объект между операциями.
type T struct { A int32 B int64}var t Toff := unsafe.Offsetof(t.B) // 8 (из-за выравнивания)pb := (*int64)(unsafe.Add(unsafe.Pointer(&t), off)) // Go 1.17+: unsafe.Add_ = pbЧем отличается статическая типизация от динамической?
Заголовок раздела «Чем отличается статическая типизация от динамической?»Коротко. При статической типизации тип каждого выражения известен и проверяется на этапе компиляции; при динамической — тип носит значение во время выполнения, и проверки происходят в рантайме. Go статически типизирован.
Глубже. Практическая разница — где вы получаете ошибку: x := "a" + 1 в Go не соберётся, в Python упадёт в рантайме. Статическая типизация даёт нулевую стоимость проверок в рантайме, точный layout структур в памяти и работающий автокомплит/рефакторинг; цена — многословность и необходимость генериков или интерфейсов там, где динамический язык обошёлся бы утиной типизацией. Важно: Go не «полностью статичен» — у интерфейсов есть динамический тип, который проверяется в рантайме при type assertion и type switch, и это единственная легальная точка динамики (не считая reflect).
Чем отличается строгая (сильная) типизация от нестрогой (слабой)?
Заголовок раздела «Чем отличается строгая (сильная) типизация от нестрогой (слабой)?»Коротко. Сильная типизация означает, что язык не делает неявных преобразований типов и не позволяет трактовать биты одного типа как другой; слабая — допускает неявные приведения и «магию» вроде "1" + 1. Go сильно типизирован, причём строже большинства: даже int и int64 не смешиваются без явной конвертации.
Глубже. Термины «сильная/слабая» нестрогие и часто путаются со статической/динамической; корректно отвечать через примеры. В Go нет ни числового promotion (int32 + int64 — ошибка компиляции), ни неявного bool ↔ int, ни неявного приведения именованного типа к его базовому в бинарных операциях. Единственные послабления: нетипизированные константы (const c = 1 подстраивается под контекст, поэтому var f float64 = 1 работает), и присваивание между типом и его именованным алиасом или между типами с идентичным underlying type, если хотя бы один из них не именованный. Лазейки в сильную типизацию есть только через unsafe.Pointer и reflect.
type Celsius float64var c Celsius = 36.6var f float64 = c // ошибка компиляции: разные типыvar g float64 = float64(c) // окЧто такое референсные типы?
Заголовок раздела «Что такое референсные типы?»Коротко. См. выше про «ссылочные типы» — это тот же вопрос, просто калькой с английского reference types. Кратко: slice, map, chan, func, interface, *T; значение содержит указатель, но передаётся всё равно копией.
Глубже. Единственное отличие формулировки: если интервьюер настаивает на термине, стоит явно сказать, что спецификация Go его не использует, и добавить критерий «нулевое значение — nil, работоспособный экземпляр создаётся через make или литерал/new». Под этот критерий попадают ровно slice, map, chan, func, interface, *T.
Какие есть строенные типы в Go?
Заголовок раздела «Какие есть строенные типы в Go?»Коротко. Вопрос про встроенные (предопределённые) типы. Это: bool; string; целые int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr; вещественные float32, float64; комплексные complex64, complex128; алиасы byte (= uint8) и rune (= int32); плюс any (алиас interface{} с Go 1.18), error и comparable (ограничение для генериков).
Глубже. Отдельно от предопределённых типов стоят типообразующие конструкции: массив, срез, структура, указатель, функция, интерфейс, мапа, канал. error — это не «магический» тип, а обычный предопределённый интерфейс с методом Error() string. comparable — не тип, а интерфейс-ограничение, использовать его можно только как constraint. Ещё есть предопределённые константы true, false, iota и значение nil.
Что можете сказать про устройство планировщка в Go?
Заголовок раздела «Что можете сказать про устройство планировщка в Go?»Коротко. Планировщик Go — кооперативно-вытесняющий, работает в user space по модели M:N и обозначается аббревиатурой GMP: G — горутина, M — поток ОС, P — логический процессор (контекст выполнения, их по умолчанию GOMAXPROCS). Каждый P держит локальную очередь горутин, есть глобальная очередь, и простаивающий P крадёт работу у соседей (work stealing).
Глубже. Ключевые детали, которые ждут услышать. Локальная runq на P — 256 элементов плюс слот runnext для только что разбуженной горутины (это оптимизация локальности для паттерна «пинг-понг» по каналу). При переполнении локальной очереди половина уезжает в глобальную. Каждый 61-й тик планировщик обязательно заглядывает в глобальную очередь, чтобы не заморить её голодом. Блокирующие сетевые операции не блокируют M: они уходят в netpoller (epoll/kqueue/IOCP), горутина паркуется, M берёт следующую. Блокирующий системный вызов (файлы, cgo) отвязывает M от P — handoffp отдаёт P другому потоку, поэтому число M может сильно превышать GOMAXPROCS (лимит 10000). Фоновый поток sysmon без P мониторит систему: отбирает P у горутин, работающих дольше ~10 мс, retake’ит P у долгих syscall’ов, форсит GC. С Go 1.14 вытеснение асинхронное — через сигнал SIGURG и safe-points, поэтому «горячий» цикл без вызовов функций больше не вешает планировщик. С Go 1.21+ добавлены улучшения в порядке пробуждения, а с Go 1.24 GOMAXPROCS по-прежнему не учитывает cgroup-лимиты автоматически (это меняется в более поздних версиях, поэтому в контейнерах традиционно ставят automaxprocs).
Как распределялись задачи между Java и Go?
Заголовок раздела «Как распределялись задачи между Java и Go?»Коротко. Это вопрос про личный опыт: интервьюер проверяет, понимаете ли вы, почему в компании два стека и по какому критерию выбирают язык, а не просто «так исторически сложилось».
Глубже. Каркас ответа: (1) контекст — что за система, сколько сервисов, кто владелец каждого стека; (2) критерий разделения, названный явно. Типичные честные критерии: Java — там, где тяжёлая бизнес-логика, зрелая экосистема (Spring, Kafka Streams, интеграции с легаси, ORM, batch-обработка, big data), и там, где команда уже большая; Go — сетевые сервисы с высокой конкурентностью и требованием к latency/потреблению памяти, шлюзы, прокси, sidecar-сервисы, инфраструктурные утилиты и CLI, короткоживущие serverless-функции (быстрый старт без прогрева JIT). (3) Точки стыка: общий контракт (protobuf/gRPC или OpenAPI), общая шина, общая observability. (4) Что было больно: дублирование библиотек (два SDK для одного и того же), разные подходы к конфигам и трассировке, разный уровень зрелости кода. Ошибка кандидата — оценочные суждения «Java медленная, Go быстрый»: вместо этого называйте измеримые критерии (p99, RSS, время старта, скорость найма).
Как объявить функцию принимающую переменное количество аргументов?
Заголовок раздела «Как объявить функцию принимающую переменное количество аргументов?»Коротко. Последний параметр объявляется как ...T; внутри функции он виден как []T. Вызывать можно перечислением аргументов или передав готовый слайс с slice....
Глубже. Нюансы: вариативный параметр обязан быть последним и только один; при вызове без аргументов внутри будет nil-слайс (не пустой, но len == 0, что для большинства кода одинаково); при вызове f(s...) слайс не копируется — функция получает тот же массив и может испортить данные вызывающего; при обычном вызове f(a, b, c) компилятор создаёт временный слайс (иногда на стеке, если он не escape’ится). Тип func(...int) и func([]int) — разные типы, не взаимозаменяемые.
func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total}
func main() { fmt.Println(sum()) // 0 fmt.Println(sum(1, 2, 3)) // 6 xs := []int{1, 2, 3} fmt.Println(sum(xs...)) // 6, xs не копируется}Какие встроенные примитивные типы данных существуют?
Заголовок раздела «Какие встроенные примитивные типы данных существуют?»Коротко. Булев bool; строковый string; знаковые целые int8/16/32/64 и платформозависимый int; беззнаковые uint8/16/32/64, uint, uintptr; вещественные float32, float64; комплексные complex64, complex128. Плюс алиасы byte = uint8 и rune = int32.
Глубже. Строго «примитивным» в терминах памяти можно считать всё, кроме string: строка — двухсловный дескриптор (ptr, len) с иммутабельными байтами. Целые в Go имеют определённое поведение при переполнении: знаковое и беззнаковое переполнение — это wrap-around по модулю 2^n, без UB, как в C. Сдвиг на число ≥ разрядности даёт 0 (а не UB). Деление на нулевую константу — ошибка компиляции, на нулевую переменную — паника runtime error: integer divide by zero. Для чисел с плавающей точкой действует IEEE 754; сравнение NaN != NaN, а float64 тип comparable, но NaN ломает использование в качестве ключа мапы.
Какой тип использовал бы для хранения больших чисел в Go?
Заголовок раздела «Какой тип использовал бы для хранения больших чисел в Go?»Коротко. Если число не влезает в int64/uint64 — стандартный пакет math/big: big.Int для целых произвольной точности, big.Float для вещественных с заданной mantissa, big.Rat для точных дробей. Для денег ни в коем случае не float64: целочисленные минорные единицы (копейки) в int64 либо decimal-библиотека.
Глубже. math/big — арифметика с произвольной точностью на слайсе слов, аллоцирующая: методы имеют форму «receiver — приёмник результата» (z.Add(x, y)), чтобы переиспользовать буферы. Она заметно медленнее машинных чисел, поэтому для «просто больших, но ограниченных» значений сначала проверьте, хватает ли uint64 (до ~1.8·10^19) или пары слов. Для криптографии — crypto/rand + big.Int, и обязательно константное время у операций (в math/big его нет, для этого есть crypto/internal и специализированные пакеты). Для денег на практике берут int64 в минимальных единицах или github.com/shopspring/decimal / github.com/govalues/decimal.
a, _ := new(big.Int).SetString("123456789012345678901234567890", 10)b := big.NewInt(2)fmt.Println(new(big.Int).Mul(a, b)) // 246913578024691357802469135780Как реализовать Set в Go?
Заголовок раздела «Как реализовать Set в Go?»Коротко. Встроенного типа нет. Канонический вариант — map[T]struct{}: struct{} занимает 0 байт, поэтому хранятся только ключи. Если нужен ещё и «признак», удобнее map[T]bool, но он тратит байт на значение.
Глубже. Ключ обязан быть comparable (числа, строки, bool, указатели, каналы, интерфейсы, массивы и структуры из comparable-полей — но не слайсы/мапы/функции). Для не-comparable элементов сет строят по производному ключу (хеш, сериализация, id). Для конкурентного доступа — sync.Map (оптимизирован под «много читателей, редкие записи») или обычная мапа под sync.RWMutex. Для очень больших множеств и приблизительного ответа — bitset (math/bits + []uint64) или Bloom-фильтр. С Go 1.18 сет удобно оформить дженериком.
type Set[T comparable] map[T]struct{}
func (s Set[T]) Add(v T) { s[v] = struct{}{} }func (s Set[T]) Has(v T) bool { _, ok := s[v]; return ok }func (s Set[T]) Delete(v T) { delete(s, v) }func (s Set[T]) Len() int { return len(s) }Как в Golang можно реализовать множество (set)?
Заголовок раздела «Как в Golang можно реализовать множество (set)?»Коротко. См. выше — map[T]struct{} (или map[T]bool), при необходимости обёрнутый в дженерик-тип с методами Add/Has/Delete.
Глубже. Отличие от предыдущего вопроса — здесь уместно перечислить операции над множествами и их сложность: объединение/пересечение/разность делаются линейным проходом по меньшей мапе, каждая операция O(1) в среднем. Если требуется упорядоченное множество — мапы не подойдут, порядок обхода намеренно рандомизирован: берут отсортированный слайс с бинарным поиском (slices.BinarySearch, Go 1.21+) или дерево из сторонней библиотеки. Если нужен детерминированный вывод — собирайте ключи в слайс и сортируйте slices.Sorted(maps.Keys(m)) (Go 1.23+, maps.Keys возвращает итератор).
Что такое указатель и сколько он весит?
Заголовок раздела «Что такое указатель и сколько он весит?»Коротко. Указатель *T — значение, хранящее адрес переменной типа T. Размер равен машинному слову платформы: 8 байт на 64-битных архитектурах (amd64, arm64), 4 байта на 32-битных (386, arm). Проверяется через unsafe.Sizeof.
Глубже. Нулевое значение — nil, разыменование nil даёт панику invalid memory address or nil pointer dereference (рантайм ловит SIGSEGV и превращает в панику). Арифметики указателей нет; сравнивать можно на равенство. В Go указатель можно взять на что угодно адресуемое, включая локальную переменную — компилятор сам решает escape-анализом, оставить её на стеке или перенести в кучу, поэтому «висячих указателей» не бывает. Размер самого указателя не зависит от размера T: *[1<<20]byte — те же 8 байт. Отдельно помните, что «указательные» по сути типы (map, chan, func) тоже весят одно слово, а слайс — три, интерфейс и строка — по два.
var p *intfmt.Println(unsafe.Sizeof(p)) // 8 на amd64/arm64fmt.Println(unsafe.Sizeof(uintptr(0))) // то же самоеКакие бывают целочисленные типы данных в Go?
Заголовок раздела «Какие бывают целочисленные типы данных в Go?»Коротко. Знаковые: int8, int16, int32, int64 и платформозависимый int. Беззнаковые: uint8, uint16, uint32, uint64, платформозависимый uint и uintptr (целое, достаточное для хранения адреса). Алиасы: byte = uint8, rune = int32.
Глубже. Все они — разные типы, даже если размер одинаков: int и int64 на amd64 занимают 8 байт, но присвоить одно другому без конвертации нельзя. Диапазоны: знаковый n-битный — от −2^(n−1) до 2^(n−1)−1, беззнаковый — от 0 до 2^n−1; константы math.MaxInt64, math.MaxInt (Go 1.17+) и т. д. uintptr отличается от unsafe.Pointer тем, что не считается указателем для GC: объект, на который «смотрит» только uintptr, может быть собран или перемещён вместе со стеком.
Чем отличается int8 от int32?
Заголовок раздела «Чем отличается int8 от int32?»Коротко. Разрядностью и, как следствие, диапазоном: int8 — 1 байт, значения −128…127; int32 — 4 байта, −2 147 483 648…2 147 483 647. Это два разных типа, между ними нужна явная конвертация.
Глубже. Практические следствия. Переполнение молчаливое: int8(127) + 1 == -128. Конвертация вниз обрезает старшие биты: int8(int32(300)) == 44. В структурах разница влияет на layout и выравнивание — поле int32 выравнивается по 4 байтам, int8 по 1, поэтому порядок полей меняет unsafe.Sizeof структуры. int32 — это ещё и rune, тип для кодовой точки Unicode, а int8 в реальном коде встречается редко (в основном в бинарных протоколах и cgo). Арифметика на маленьких типах на современных CPU не быстрее — выигрыш только в памяти, на больших массивах.
По какому принципу выбирается целочисленный тип для задачи?
Заголовок раздела «По какому принципу выбирается целочисленный тип для задачи?»Коротко. По умолчанию — int: это «естественный» размер слова, тип индексов, len, cap. Конкретную разрядность берут, когда её диктует внешний контракт (бинарный протокол, формат файла, схема БД, cgo) или когда важен объём памяти на больших массивах.
Глубже. Правила, которые звучат убедительно: (1) int для индексов, счётчиков, размеров — API стандартной библиотеки говорит на int, и постоянные конвертации только добавят багов; (2) фиксированный размер — там, где значение сериализуется или пересекает границу процесса, чтобы одинаково вести себя на 32- и 64-битных платформах; (3) uint брать не «потому что число неотрицательное», а только если нужны биты старшего разряда или битовые операции — беззнаковые типы дают классический баг for i := uint(0); i <= n; i-- и «отрицательную» разницу, которая превращается в огромное число; (4) int64 для времени, денег в минорных единицах, идентификаторов; (5) на больших массивах (миллионы элементов) стоит посчитать память: []int32 вдвое легче []int64, что часто важнее арифметики. Всегда проверяйте сужающие конвертации: int64 → int32 молча обрезает, safe-обёртка — сравнение с math.MaxInt32 перед конвертацией.
Можем передать функцию в качестве параметра?
Заголовок раздела «Можем передать функцию в качестве параметра?»Коротко. Да. Функции в Go — значения первого класса: их можно передавать в параметрах, возвращать, хранить в переменных, полях структур, слайсах и мапах.
Глубже. Тип параметра описывается сигнатурой: func(cb func(int) error). Значение функции — одно слово: указатель на структуру funcval, где лежит адрес кода и, если это замыкание, захваченные переменные. Отсюда два следствия: вызов через переменную-функцию — косвенный (обычно не инлайнится, хотя компилятор умеет девиртуализировать очевидные случаи), и замыкание может аллоцировать в куче захваченные переменные. Метод тоже можно передать как значение: v.Method — это method value (замыкание с захваченным receiver), T.Method — method expression (обычная функция с receiver первым аргументом). Функции сравнимы только с nil; сравнивать две функции между собой нельзя.
func apply(xs []int, f func(int) int) []int { out := make([]int, len(xs)) for i, x := range xs { out[i] = f(x) } return out}_ = apply([]int{1, 2}, func(x int) int { return x * x })Что относят к типу функции?
Заголовок раздела «Что относят к типу функции?»Коротко. Тип функции определяется только списком типов параметров (включая признак вариативности) и списком типов результатов. Имена параметров, имя самой функции и наличие/отсутствие именованных результатов в тип не входят.
Глубже. То есть func(a, b int) (sum int, err error) и func(int, int) (int, error) — один и тот же тип. Метод в тип функции не превращается автоматически: у метода есть receiver, и типом func(...) он становится только через method value/expression. Вариативность — часть типа: func(...int) ≠ func([]int). Именованный функциональный тип (type Handler func(w http.ResponseWriter, r *http.Request)) можно наделять методами — так, например, устроен http.HandlerFunc, который реализует интерфейс http.Handler. Нулевое значение функционального типа — nil, вызов такой переменной паникует.
Что такое анонимная функция?
Заголовок раздела «Что такое анонимная функция?»Коротко. Анонимная функция — функциональный литерал без имени: func(x int) int { return x * 2 }. Её можно сразу вызвать, присвоить переменной, передать аргументом; если она обращается к переменным окружения, она становится замыканием (closure).
Глубже. Замыкание захватывает переменные по ссылке, а не по значению: переменная, захваченная литералом и живущая дольше кадра стека, переносится компилятором в кучу (escape-анализ). Отсюда классическая ловушка с циклом — и важное изменение: с Go 1.22 переменная цикла создаётся заново на каждой итерации, поэтому код ниже теперь печатает 0,1,2 в произвольном порядке, а до Go 1.22 печатал 3,3,3 (при go directive в go.mod ≥ 1.22). Ещё частое место анонимных функций — defer func(){ ... }() для восстановления после паники и правки именованных результатов, и горутины go func(){ ... }().
for i := 0; i < 3; i++ { go func() { fmt.Println(i) }() // Go 1.22+: 0,1,2; до 1.22: обычно 3,3,3}Как реализовано ооп в Go?
Заголовок раздела «Как реализовано ооп в Go?»Коротко. В Go нет классов и наследования. ООП собирается из трёх кирпичей: структуры с методами (методы объявляются вне типа, с receiver), встраивание структур/интерфейсов для переиспользования (композиция вместо наследования) и интерфейсы, которые реализуются неявно — тип соответствует интерфейсу, если у него есть нужные методы.
Глубже. Инкапсуляция в Go — на уровне пакета, а не типа: идентификатор с заглавной буквы экспортируется, со строчной — виден только внутри пакета (включая другие типы того же пакета). Полиморфизм — только динамический через интерфейс: значение интерфейса — пара (itab, data), вызов метода идёт через таблицу в itab. Встраивание (type Admin struct { User }) продвигает методы и поля встроенного типа наверх, но это делегирование, а не наследование: нет виртуальных методов, нет переопределения с вызовом «родителя» через super, и, главное, метод встроенного типа, вызывающий другой метод, всегда вызовет собственную реализацию, а не «переопределённую» во внешнем типе (нет позднего связывания). Отдельно помните про receiver: методы с value receiver входят в method set и T, и *T; методы с pointer receiver — только в method set *T, поэтому var _ Iface = T{} может не скомпилироваться, а var _ Iface = &T{} — да.
type Reader interface{ Read(p []byte) (int, error) }
type Logger struct{ prefix string }func (l Logger) Log(msg string) { fmt.Println(l.prefix, msg) }
type Service struct { Logger // встраивание: у Service появляется метод Log name string}Есть ли какие-то нюансы с типом int (имеется в виду зависимость от разрядности системы)?
Заголовок раздела «Есть ли какие-то нюансы с типом int (имеется в виду зависимость от разрядности системы)?»Коротко. Да: int и uint — платформозависимые, 64 бита на amd64/arm64 и 32 бита на 386/arm. Поэтому int нельзя использовать там, где размер важен для корректности: в бинарной сериализации, в межпроцессных контрактах, в вычислениях, которые могут переполниться на 32-битной платформе.
Глубже. Размер int равен размеру uintptr на всех поддерживаемых платформах, но формально это не гарантия спецификации — гарантируется лишь «не менее 32 бит» и одинаковый размер у int и uint. Практические следствия: значение до ~2·10^9 безопасно везде, дальше на 32-битной сборке будет молчаливое переполнение; len() возвращает int, поэтому максимальный размер слайса на 32-битной системе ограничен; int и int64 — разные типы, конвертация обязательна, и int64 → int на 32-битной платформе обрезает. Узнать размер во время выполнения: strconv.IntSize или bits.UintSize (обе константы — 32 или 64). Для «портируемых» проверок диапазона используйте math.MaxInt / math.MinInt (появились в Go 1.17).
fmt.Println(bits.UintSize) // 64 на amd64fmt.Println(unsafe.Sizeof(int(0))) // 8 на amd64, 4 на 386Перечислить типы данных Go;
Заголовок раздела «Перечислить типы данных Go;»Коротко. Базовые: bool, string, целые (int, int8/16/32/64, uint, uint8/16/32/64, uintptr), вещественные (float32/64), комплексные (complex64/128), алиасы byte, rune. Композитные: массив, срез, структура, указатель, функция, интерфейс, мапа, канал.
Глубже. Хороший ответ добавляет размеры и семантику: массив и структура — значения, копируются целиком; срез — 3 слова, разделяет массив; строка — 2 слова, иммутабельна; мапа и канал — по слову-указателю, требуют make; интерфейс — 2 слова (тип, данные); функция — слово. Ещё стоит упомянуть, что пользовательские типы создаются через type Name Underlying (новый тип, свой method set) и type Name = Underlying (алиас, тот же тип), а с Go 1.24 алиасы могут быть обобщёнными (type Set[T comparable] = map[T]struct{}).
Какие плюсы и минусы Go?
Заголовок раздела «Какие плюсы и минусы Go?»Коротко. Плюсы: простой язык с быстрым порогом входа, конкурентность в языке (горутины, каналы, select), быстрая компиляция в один статический бинарник, мощный runtime с GC с субмиллисекундными паузами, отличный tooling из коробки (go test, pprof, race detector, go vet, gofmt) и жёсткая гарантия обратной совместимости. Минусы: многословная обработка ошибок, отсутствие sum-типов и enum’ов, nil как источник паник, ограниченные дженерики, слабая выразительность в сравнении с Rust/Kotlin, GC (не подходит для hard real-time), и «однообразный» код.
Глубже. Аргументируйте примерами, а не вкусом. Компиляция: миллион строк собирается за секунды, потому что зависимости импортируются через скомпилированные пакеты, нет заголовочных файлов и циклических импортов. Совместимость: Go 1 compatibility promise + механизм языковых версий в go.mod (изменение семантики цикла в 1.22 включается только при go >= 1.22) — редкий пример эволюции языка без разлома экосистемы. Из минусов честно назовите: отсутствие проверяемых на этапе компиляции исчерпывающих switch, нулевое значение вместо Option, интерфейсная nil-ловушка, дженерики без методов с параметрами типа, context.Context как обязательный первый аргумент везде, и повторяющийся if err != nil (попытка ввести try была отклонена сообществом).
Расскажи про ооп в Go?
Заголовок раздела «Расскажи про ооп в Go?»Коротко. См. выше «Как реализовано ООП в Go»: структуры + методы, композиция и встраивание вместо наследования, неявно реализуемые интерфейсы, инкапсуляция на уровне пакета.
Глубже. Что стоит добавить, если вопрос повторяют: Go относится к ООП утилитарно — «объекты» есть, «объектной иерархии» нет. Практические принципы: интерфейсы объявляются на стороне потребителя, а не производителя, и делаются маленькими (io.Reader — один метод); «accept interfaces, return structs»; вместо абстрактных базовых классов — встраивание интерфейса в структуру (частичная реализация с делегированием); вместо конструкторов — функции NewX, вместо перегрузки — варианты имён или функциональные опции. Из SOLID в Go естественно ложатся ISP и DIP, а LSP теряет смысл без наследования.
Какие типы баз данных бывают?
Заголовок раздела «Какие типы баз данных бывают?»Коротко. Реляционные (PostgreSQL, MySQL), key-value (Redis, etcd), документные (MongoDB), колоночные/аналитические (ClickHouse, BigQuery), wide-column (Cassandra, HBase), графовые (Neo4j), time-series (Prometheus, TimescaleDB, VictoriaMetrics), поисковые (Elasticsearch, OpenSearch), а также in-memory и встраиваемые (SQLite, BadgerDB, BoltDB).
Глубже. Классифицировать полезнее не по модели данных, а по свойствам, которые следуют из движка. OLTP против OLAP: строковое хранение и B-дерево против колоночного хранения и векторизованного выполнения. LSM-дерево (RocksDB, Cassandra, ClickHouse) даёт быструю запись и compaction-нагрузку, B+-дерево (PostgreSQL, InnoDB) — предсказуемое чтение. Модель консистентности: строгая (одиночный Postgres), настраиваемая кворумная (Cassandra), eventual. Транзакции и уровни изоляции. Для Go-собеседования уместно связать это с практикой: database/sql + pgx для Postgres, пул соединений (SetMaxOpenConns, SetMaxIdleConns, SetConnMaxLifetime), обязательный rows.Close() и rows.Err(), контекстные варианты методов (QueryContext), и почему для аналитики берут отдельное хранилище, а не «ещё один индекс в OLTP».
Какой у вас опыт работы с Go?
Заголовок раздела «Какой у вас опыт работы с Go?»Коротко. HR/вводный вопрос. Ждут структурированный рассказ: сколько лет и в каких доменах, какого масштаба были сервисы, за что вы конкретно отвечали, какие сложные технические задачи решали.
Глубже. Каркас на 1.5–2 минуты: (1) стаж и контекст — «около N лет на Go, до этого X; последние два года — платформенная команда, микросервисы на gRPC, PostgreSQL, Kafka»; (2) масштаб цифрами — RPS, объём данных, количество инстансов, SLA/latency; (3) одна-две конкретные задачи, где вы принимали решение, с обоснованием и результатом («убрали лишние аллокации в hot path, p99 упал с 120 до 40 мс — нашли через pprof»); (4) чем занимались помимо кода: код-ревью, дизайн-доки, менторинг, on-call; (5) в чём именно вы сильны в Go (конкурентность, профилирование, работа с БД, gRPC/protobuf). Типичные ошибки: перечисление технологий без роли и результата, «мы» вместо «я», рассказ на 10 минут, и завышение уровня — дальше будут копать вглубь именно в то, что вы назвали.
Почему менеджер в ОС медленнее чем шудулер в Go?
Заголовок раздела «Почему менеджер в ОС медленнее чем шудулер в Go?»Коротко. Точнее говорить не «медленнее», а «дороже одно переключение». Переключение горутин делает сам процесс в user space: это сохранение трёх регистров (PC, SP, указатель на g) и переход к другой горутине — десятки наносекунд. Переключение потоков ОС требует входа в ядро, сохранения полного контекста регистров, работы с планировщиком ядра и, при смене процесса, перезагрузки страничных таблиц с инвалидацией TLB — единицы микросекунд.
Глубже. Причины разницы: (1) нет системного вызова и смены кольца защиты; (2) Go сохраняет минимум состояния, потому что переключение чаще происходит в известной точке (safe point у вызова функции), а не в произвольном месте по прерыванию таймера; (3) стек горутины начинается с 8 КБ (2 КБ в старых версиях) и растёт копированием, поэтому создать миллион горутин реально, а миллион потоков — нет (у потока стек по умолчанию мегабайты, плюс структура задачи в ядре); (4) блокировка на канале или мьютексе не превращается в блокировку потока — планировщик просто ставит на M другую G; (5) сетевой I/O уходит в netpoller, так что «блокирующий» код не блокирует поток. Но обязательно оговоритесь, что сравнение нечестное: планировщик ОС решает более широкую задачу — вытесняющая многозадачность для взаимно недоверяющих процессов, приоритеты, cgroups, изоляция памяти, работа с прерываниями. Планировщик Go — кооперативный внутри одного доверенного процесса и в конечном счёте всё равно исполняется на потоках ОС.
Какие есть механизмы синхронизации в Go?
Заголовок раздела «Какие есть механизмы синхронизации в Go?»Коротко. Каналы и select как основной идиоматический способ («не общайтесь через разделяемую память — делите память через общение»), пакет sync (Mutex, RWMutex, WaitGroup, Once, Cond, Pool, Map), пакет sync/atomic (атомарные операции и типы atomic.Int64, atomic.Pointer[T] с Go 1.19), а также context для отмены и golang.org/x/sync (errgroup, semaphore, singleflight).
Глубже. Что важно сказать сверх списка. sync.Mutex — не рекурсивный, разблокировать может любая горутина, но идиоматично та же; с Go 1.9 есть starvation mode: если горутина ждёт >1 мс, мьютекс переходит в «честный» режим и передаёт владение в порядке очереди. RWMutex выгоден только при действительно длинных чтениях — на коротких критических секциях он медленнее обычного Mutex из-за атомарных счётчиков. WaitGroup: Add вызывается до запуска горутины, а с Go 1.25 появился wg.Go(fn), устраняющий классическую ошибку. sync.Once гарантирует, что все ожидающие увидят результат инициализации (happens-before). sync.Pool — не кеш: содержимое чистится при каждом GC. atomic даёт lock-free счётчики и подмену указателя, но не заменяет мьютекс для составных инвариантов. Для «нескольких горутин с ошибкой и отменой» — errgroup.WithContext. И всегда упоминайте go test -race: гонки в Go — не UB, но поведение при них не определено на уровне модели памяти (описана в «The Go Memory Model»).
Что можешь сказать про ООП и как это реализовано в Go?
Заголовок раздела «Что можешь сказать про ООП и как это реализовано в Go?»Коротко. См. выше — структуры с методами, композиция/встраивание вместо наследования, неявные интерфейсы, инкапсуляция на уровне пакета; из «трёх китов» ООП в Go есть инкапсуляция и полиморфизм, наследования нет.
Глубже. Отличие от предыдущих формулировок вопроса — здесь стоит показать, что вы понимаете цену решений. Неявные интерфейсы дают развязку пакетов (потребителю не нужно импортировать пакет реализации), но лишают компилятор возможности проверить «я хотел реализовать этот интерфейс» — отсюда идиома var _ io.Reader = (*MyType)(nil). Динамическая диспетчеризация через itab мешает инлайну, поэтому в горячем коде интерфейс иногда заменяют дженериком или конкретным типом. Встраивание сокращает бойлерплейт, но легко приводит к неожиданным конфликтам имён (при одинаковой глубине — ошибка компиляции при обращении, при разной — побеждает более внешний) и к «случайно экспортированному» API встроенного типа.
Что вас огорчает в системе типов Go?
Заголовок раздела «Что вас огорчает в системе типов Go?»Коротко. Чаще всего называют: отсутствие sum-типов/алгебраических типов и настоящих enum’ов, отсутствие Option/non-nullable указателей (нулевое значение вместо «значение отсутствует»), nil-ловушку интерфейсов, ограниченность дженериков и невозможность выразить неизменяемость.
Глубже. Конкретика, которая показывает опыт. (1) Нет исчерпывающего switch по закрытому множеству вариантов — компилятор не подскажет забытую ветку, iota-«enum» — это просто числа, и в него можно положить любое значение. (2) nil-интерфейс с ненулевым динамическим типом: возврат *MyErr(nil) в error делает err != nil истинным — самая известная ловушка. (3) Дженерики (1.18+): нельзя объявить метод с собственным параметром типа, нет вариантности, нельзя ограничить «тип с полем», type switch по параметру типа не поддерживается, вывод типов местами капризен; до 1.24 не было обобщённых алиасов. (4) Нет const для структур и полей — иммутабельность обеспечивается только соглашением и копированием. (5) Нельзя выразить «этот интерфейс реализуют только эти типы» иначе как трюком с неэкспортируемым методом. (6) Ошибки — интерфейс, поэтому проверка вида «какие ошибки может вернуть функция» не выражается в типе. Хороший ответ заканчивается балансом: эти ограничения — цена за простоту чтения и скорость компиляции, и в большинстве продуктовых задач они терпимы.
Какие средства обобщенного программирования есть в Go?
Заголовок раздела «Какие средства обобщенного программирования есть в Go?»Коротко. До Go 1.18 — интерфейсы (interface{}) с type assertion/type switch, reflect и кодогенерация (go generate). С Go 1.18 — настоящие дженерики: параметры типа у функций и типов, интерфейсы-ограничения с наборами типов, any, comparable, вывод типов.
Глубже. Реализация — GC shape stenciling: компилятор генерирует по одной копии кода на «форму» типа (все указательные типы делят одну инстанциацию), а различия передаются через скрытый параметр-словарь. Отсюда — дженерик-код с указателями почти всегда медленнее мономорфизированного и часто медленнее ручного кода; выигрыш дженериков — в типобезопасности, а не в производительности. Ограничения: методы не могут иметь своих параметров типа; ограничения выражаются через интерфейсы с union-элементами (~int | ~float64, где ~ означает «любой тип с таким underlying»); comparable до Go 1.20 не принимал интерфейсы, с 1.20 принимает (со стороны реализации сравнение может паниковать). Обобщённые алиасы типов появились в Go 1.24. В стандартной библиотеке дженерики дали пакеты slices, maps, cmp (Go 1.21) и min/max/clear как встроенные функции. Начиная с Go 1.23 обобщённый код удобно комбинировать с итераторами (iter.Seq[T], range-over-func).
func MapSlice[T, U any](xs []T, f func(T) U) []U { out := make([]U, 0, len(xs)) for _, x := range xs { out = append(out, f(x)) } return out}
type Number interface{ ~int | ~int64 | ~float64 }func Sum[T Number](xs []T) T { var s T; for _, x := range xs { s += x }; return s }Что такое тип-сумма и как ее реализовать на Go?
Заголовок раздела «Что такое тип-сумма и как ее реализовать на Go?»Коротко. Тип-сумма (sum type, tagged union) — тип, значение которого равно ровно одному из фиксированного набора вариантов, каждый со своими данными. В Go его нет на уровне языка; ближе всего — «закрытый интерфейс»: интерфейс с неэкспортируемым методом-маркером, который могут реализовать только типы из того же пакета, плюс switch v := x.(type).
Глубже. У закрытого интерфейса два минуса: компилятор не проверяет исчерпывающесть switch (нужен default: panic("unhandled") и линтер вроде exhaustive), и значение интерфейса может быть nil. Альтернативы: структура-«тег + опциональные поля» (как protobuf oneof, который генерирует ровно закрытый интерфейс), либо дженерик-обёртка Result[T]/Either[A,B] — она компилируема, но неудобна, потому что вывод типов и отсутствие pattern matching делают код многословным. Предложение о добавлении sum types в Go обсуждается годами и не принято.
type Shape interface{ isShape() }
type Circle struct{ R float64 }type Rect struct{ W, H float64 }
func (Circle) isShape() {}func (Rect) isShape() {}
func Area(s Shape) float64 { switch v := s.(type) { case Circle: return math.Pi * v.R * v.R case Rect: return v.W * v.H default: panic(fmt.Sprintf("unhandled shape %T", s)) }}Что значит “A little copying is better than a little dependency ”?
Заголовок раздела «Что значит “A little copying is better than a little dependency ”?»Коротко. Это одна из Go Proverbs Роба Пайка: если из чужой библиотеки вам нужны 20 строк, честнее скопировать эти 20 строк (с указанием источника и лицензии), чем тащить зависимость целиком. Зависимость — это не только код, а ещё её транзитивные зависимости, её баги, её релизный цикл, её supply-chain-риск и её влияние на размер бинарника и время сборки.
Глубже. В Go этот принцип виден в самой стандартной библиотеке: внутренние пакеты дублируют друг у друга мелкие хелперы, чтобы не создавать циклы импорта и лишние связи. Практическая граница: копировать имеет смысл небольшой, стабильный, легко тестируемый код (утилита, алгоритм, парсер узкого формата); не копировать — криптографию, парсеры сложных форматов, всё, что требует регулярных обновлений безопасности. Обратная сторона правила — копия не получает исправлений апстрима, поэтому её нужно покрывать своими тестами. Пословица не про «изобретать велосипед», а про осознанную цену связности.
Следите ли вы за обновлениями Go? Какие изменения последних версий считаете наиболее значимыми?
Заголовок раздела «Следите ли вы за обновлениями Go? Какие изменения последних версий считаете наиболее значимыми?»Коротко. Здесь проверяют, живёте ли вы в экосистеме. Хороший ответ: назвать источник (release notes, блог go.dev, GopherCon, changelog в CI при апгрейде) и 3–4 конкретных изменения последних версий с объяснением, почему они важны именно вам.
Глубже. Опорные факты по версиям, на которые можно ссылаться. Go 1.18 — дженерики, workspaces, fuzzing. Go 1.20 — обёртка нескольких ошибок в errors.Join, PGO в превью. Go 1.21 — встроенные min/max/clear, пакеты slices/maps/cmp, log/slog (структурированное логирование в stdlib), стабильный PGO, механизм языковых версий в go.mod и toolchain-строки. Go 1.22 — переменная цикла создаётся на каждой итерации (устранён самый частый баг с горутинами), range по целому числу, новые паттерны в net/http.ServeMux (методы и wildcard в путях), math/rand/v2. Go 1.23 — итераторы: range по функциям, пакеты iter, unique, переработанные таймеры (не нужно дренировать канал, таймеры собираются GC). Go 1.24 — мапы на Swiss Tables, обобщённые алиасы типов, tool-директива в go.mod вместо tools.go, omitzero в encoding/json, пакет weak, runtime.AddCleanup вместо SetFinalizer, os.Root для безопасной работы с каталогом, FIPS-140 режим. Скажите и о том, как вы это применяете: например, «перевели логи на slog», «после 1.22 убрали x := x из горутин», «включили PGO и получили N% CPU». Не притворяйтесь, что знаете всё, — лучше три изменения с деталями, чем десять названий.
Если присвоить возвращаемое значение функции новой переменной, что происходит со старой переменной?
Заголовок раздела «Если присвоить возвращаемое значение функции новой переменной, что происходит со старой переменной?»Коротко. Ничего: присваивание в Go копирует значение, поэтому исходная переменная остаётся неизменной и продолжает жить до конца своей области видимости. Если после этого на неё больше никто не ссылается, память освободит GC — но только для того, что лежит в куче.
Глубже. Тонкость в том, что именно копируется. Для int, struct, массива копируются все байты — две независимые переменные. Для слайса копируется заголовок, но подлежащий массив общий: изменение b[0] увидит и a. Для мапы/канала/указателя копируется указатель — объект один на двоих. Поэтому «старая переменная не меняется» верно про саму переменную, но не про данные, на которые она указывает. Второе: сборка мусора в Go не детерминирована и не привязана к выходу из области видимости — объект будет освобождён на каком-то следующем цикле GC, если он недостижим; при этом компилятор умеет «убивать» переменную раньше конца блока (liveness), поэтому runtime.KeepAlive иногда нужен при работе с unsafe/cgo. И третье: если функция возвращает указатель на локальную переменную, escape-анализ вынесет её в кучу — это легальный и частый паттерн, в отличие от C.
type Big struct{ Data [1024]byte }func make1() Big { return Big{} }
a := make1()b := a // полная копия 1 КБ; a не измениласьb.Data[0] = 1fmt.Println(a.Data[0]) // 0Есть ли у вас опыт работы с OpenAPI в проектах на Go? Какие инструменты генерации кода использовали?
Заголовок раздела «Есть ли у вас опыт работы с OpenAPI в проектах на Go? Какие инструменты генерации кода использовали?»Коротко. Вопрос про опыт; ждут названий инструментов и понимания подхода «contract first». Основные генераторы для Go: oapi-codegen (deepmap/oapi-codegen), ogen, go-swagger (OpenAPI 2.0), официальный openapi-generator (Java), а также обратный путь — swaggo/swag, который генерирует спецификацию из комментариев в коде.
Глубже. Структура ответа: (1) какой подход выбрали — spec-first (спека в репозитории, из неё генерируются серверные интерфейсы и клиенты, генерация в CI, дифф проверяется) или code-first (swag init по аннотациям — проще стартовать, но спека вечно отстаёт); (2) что именно генерировали: типы моделей, интерфейс сервера (ServerInterface + роутер под chi/echo/gin/stdlib), клиента для соседних сервисов, валидацию по схеме; (3) боли, которые стоит назвать честно: маппинг nullable/required в Go-типы (указатели против нулевых значений), oneOf/allOf без sum-типов, format: date-time и кастомные скаляры, версионирование спеки и обратная совместимость, размер сгенерированного кода; (4) сравнение: ogen строже и быстрее (генерирует собственную сериализацию без reflection), oapi-codegen гибче и популярнее. Уместно упомянуть альтернативу — gRPC/protobuf с protoc-gen-go и grpc-gateway, если контракт внутренний. Если опыта нет — так и скажите, но опишите, как бы вы это сделали.
Какие подходы к работе с null значениями в JSON вы знаете в Go?
Заголовок раздела «Какие подходы к работе с null значениями в JSON вы знаете в Go?»Коротко. Основные варианты: указатели (*string — nil означает null/отсутствие), типы sql.NullString/sql.NullInt64 с собственными маршалерами, json.RawMessage для отложенного разбора, обёртка-дженерик Optional[T] с флагом Set, и кастомные UnmarshalJSON/MarshalJSON.
Глубже. Главная сложность — различить три состояния: поля нет в JSON, поле есть и равно null, поле есть с нулевым значением ("", 0, false). Обычный encoding/json при Unmarshal в структуру просто не трогает поле, если ключа нет, и записывает нулевое значение (для указателя — nil) при null; поэтому указатель различает только «есть значение» и «нет значения», но не отличает null от отсутствия. Для PATCH-семантики применяют либо map[string]json.RawMessage (проверяем наличие ключа), либо дженерик-обёртку с двумя булями (Present, Null) и своим UnmarshalJSON. На выходе: omitempty пропускает поле, если значение «пустое» (0, "", nil, пустой слайс/мапа), но не пропускает нулевую структуру — для этого в Go 1.24 добавлен тег omitzero, который опирается на нулевое значение типа и метод IsZero(). Для БД те же поля обычно дублируются sql.Null[T] (дженерик-версия появилась в Go 1.22) или pgtype из pgx. Полезно упомянуть, что в стандартной библиотеке ведётся работа над encoding/json/v2, где семантика опциональности и производительность пересматриваются; в продакшене пока чаще берут encoding/json, jsoniter или goccy/go-json.
type Patch struct { Name *string `json:"name,omitempty"` // nil = не менять Email *string `json:"email,omitempty"` // nil = не менять, "" = очистить}Какие Go-проекты вы вели?
Заголовок раздела «Какие Go-проекты вы вели?»Коротко. Вопрос об опыте и о роли: важно слово «вели» — интервьюер хочет услышать про ответственность за результат, а не про «писал таски».
Глубже. Каркас: (1) проект и бизнес-задача одним предложением; (2) ваша роль — техлид/владелец сервиса/автор дизайна; (3) архитектурные решения, которые приняли вы, и альтернативы, которые отвергли, с причиной; (4) масштаб и цифры (RPS, объём данных, размер команды, сроки); (5) результат в измеримых терминах и что бы вы сделали иначе. Хорошо звучат конкретные технические детали: «сервис агрегации на Go, gRPC + Kafka, 12k RPS, p99 40 мс, PostgreSQL с партиционированием, graceful shutdown через context и errgroup, метрики Prometheus, PGO дал −8% CPU». Плохо: перечень технологий без вашей роли, «мы всей командой», отсутствие цифр, невозможность объяснить причину архитектурного выбора. Заранее подготовьте 2–3 таких истории разной глубины и один «провал с выводами» — его спрашивают почти всегда.
Какие сильные стороны у Go?
Заголовок раздела «Какие сильные стороны у Go?»Коротко. Простота и читаемость (маленькая спецификация, один способ форматирования), встроенная конкурентность, быстрая компиляция и один статический бинарник без рантайм-зависимостей, зрелый инструментарий из коробки (тесты, бенчмарки, race detector, pprof, vet), низкое потребление памяти и быстрый старт, и строгая обратная совместимость.
Глубже. См. также вопрос про плюсы и минусы. Что стоит добавить именно здесь — почему это выгодно бизнесу: новый разработчик читает чужой Go-код через день, а не через месяц; деплой — копирование бинарника или FROM scratch-образ на несколько мегабайт; горизонтальное масштабирование дешевле, потому что сервис держит десятки тысяч соединений на одном инстансе; апгрейд версии Go почти всегда сводится к смене строки в go.mod и даёт бесплатный прирост производительности. Плюс экосистема инфраструктуры: Docker, Kubernetes, etcd, Prometheus, Terraform написаны на Go, поэтому для облачной разработки это язык «по умолчанию».
Что такое указатель и от чего зависит его размер?
Заголовок раздела «Что такое указатель и от чего зависит его размер?»Коротко. См. выше про указатель. Размер зависит только от разрядности целевой платформы (GOARCH): 8 байт на 64-битных, 4 байта на 32-битных, и не зависит от типа, на который указатель ссылается.
Глубже. Дополнение к предыдущему ответу: в Go нет «толстых» указателей — *T всегда одно слово, даже для массива или большой структуры. Двухсловные представления бывают у интерфейсов ((itab, data)) и строк, трёхсловное — у слайса, но это уже не указатели, а дескрипторы. Проверить размер на конкретной платформе: unsafe.Sizeof(p) либо константы strconv.IntSize / bits.UintSize (для uintptr они совпадают на всех поддерживаемых архитектурах). Ещё стоит знать, что рантайм различает «указательные» и «беспойнтерные» объекты: у типа есть битовая карта указателей, и объекты без указателей ([]byte, []int) размещаются в noscan-классах и не сканируются GC — поэтому лишние указатели в структурах прямо стоят времени сборки мусора.
Что такое мэйк?
Заголовок раздела «Что такое мэйк?»Коротко. make — встроенная функция, которая создаёт и инициализирует только слайс, мапу или канал и возвращает готовое значение этого типа (не указатель). Она нужна потому, что у этих трёх типов есть внутреннее рантайм-представление, которое нельзя получить простым обнулением памяти.
Глубже. Сигнатуры: make([]T, len) / make([]T, len, cap) — выделяет массив и возвращает заголовок; make(map[K]V) / make(map[K]V, hint) — создаёт хеш-таблицу, hint заранее резервирует место, экономя ресайзы; make(chan T) — небуферизованный, make(chan T, n) — буферизованный. Отличие от new: new(T) выделяет обнулённую память под T и возвращает *T, поэтому new(map[string]int) даёт указатель на nil-мапу, писать в которую нельзя. Важные ошибки: make([]int, 5) создаёт слайс из пяти нулей, и последующий append добавит шестой элемент (нужно make([]int, 0, 5)); отрицательный или превышающий cap аргумент len — паника (или ошибка компиляции для констант); make нельзя применить к структуре, массиву или указателю.
s := make([]int, 0, 10) // len 0, cap 10m := make(map[string]int, 8) // hint на 8 элементовch := make(chan int, 1) // буфер на 1p := new(int) // *int, *p == 0Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Говорить «слайсы и мапы передаются по ссылке». В Go всё передаётся по значению; копируется дескриптор, а данные общие — и для слайса это принципиально, потому что
appendможет отвязать копию от оригинала. - Путать «ссылку» и «указатель»: в Go нет ссылок как в C++, а термина «reference type» нет даже в спецификации.
- Считать
intиint64одним и тем же типом, потому что «на моём ноутбуке 64 бита». Это разные типы, требующие явной конверсии, иintможет быть 32-битным. - Называть
int64(x)вызовом функции. Это конверсия типа, она проверяется компилятором и для констант ловит переполнение на этапе сборки. - Утверждать, что
nil-интерфейс равенnil, если в него положилиnil-указатель. Интерфейс — пара (тип, значение), иerr != nilв этом случае истинно. - Говорить, что замыкание захватывает значение переменной. Захват идёт по переменной; и не забывать, что с Go 1.22 переменная цикла новая на каждой итерации, поэтому старый совет
i := iбольше не обязателен (но безвреден). - Сравнивать float через
==и слайсы через==. Первое ломается на IEEE-754, второе просто не компилируется (кроме сравнения сnil). - Отвечать «в Go несколько куч», путая кучу с локальными кэшами аллокатора (
mcache) и стеками горутин. - Считать, что
GOMAXPROCSограничивает число потоков ОС. Он ограничивает числоP— то есть параллельно исполняемого Go-кода; потоков может быть заметно больше, а их жёсткий предел —debug.SetMaxThreads. - Называть встраивание структур наследованием и ожидать виртуальных вызовов и
super. Промотированные методы вызывают свои методы, а не переопределённые снаружи. - «Слайсы и мапы передаются по ссылке». В Go нет передачи по ссылке — копируется дескриптор; отсюда и разница в поведении
s[i] = xиappendвнутри функции. - Путают статическую/динамическую типизацию с сильной/слабой и называют Go «строго типизированным, потому что типы проверяются при компиляции» — это две независимые оси.
- Говорят, что «в Go есть наследование через встраивание». Встраивание — делегирование с продвижением методов, без переопределения и позднего связывания.
- Считают, что
int— это всегда 64 бита, и используют его в бинарных форматах и сериализации. - Забывают, что методы с pointer receiver не входят в method set значения
T, и не понимают, почемуvar _ Iface = T{}не компилируется. - Отвечают про цикл и горутины по-старому («будет 3,3,3») — с Go 1.22 переменная цикла своя на каждой итерации.
- Называют
sync.Poolкешем и складывают туда данные, которые обязаны пережить GC. - Утверждают, что дженерики в Go ускоряют код: реализация через GC shape stenciling со словарём обычно медленнее мономорфизации.
- На вопрос про опыт перечисляют технологии без своей роли, цифр и результата.
Что почитать
Заголовок раздела «Что почитать»- The Go Programming Language Specification — типы, нулевые значения, конверсии, управляющие конструкции, правила
break/continueс метками. - Effective Go — идиомы: нулевое значение, встраивание, интерфейсы, конкурентность.
- The Go Memory Model — happens-before для каналов, мьютексов,
sync.Onceи атомиков. - Go Modules Reference —
go.mod,go.sum, MVS, checksum database. - Go Wiki: LoopvarExperiment и Go 1.22 Release Notes — новая семантика переменной цикла.
- Package unsafe и Package sync — правила работы с
unsafe.Pointerи полный список примитивов синхронизации. - The Go Programming Language Specification — https://go.dev/ref/spec (типы, method sets, вариативные функции, конвертации)
- Effective Go — https://go.dev/doc/effective_go (встраивание, интерфейсы,
newvsmake) - Go Release Notes — https://go.dev/doc/devel/release (изменения 1.21–1.24:
slices/maps, loopvar, итераторы, swiss maps) - Go Proverbs, Rob Pike — https://go-proverbs.github.io/ (в т. ч. «A little copying is better than a little dependency»)
- Исходники планировщика:
src/runtime/proc.goи дизайн-док «Scalable Go Scheduler» — https://golang.org/s/go11sched - The Go Memory Model — https://go.dev/ref/mem (что гарантируют каналы, мьютексы и atomic)
Список исходных вопросов с привязкой к компаниям: ../questions/basics.md