1Объясните, как в Go работают нулевые значения для встроенных и ссылкоподобных типов и почему они важны при объявлении переменных без явной инициализации.
В Go переменная, объявленная без явного инициализатора, автоматически инициализируется нулевым значением своего типа. Числовые типы получают 0, bool — false, string — "", а массивы и struct обнуляются поэлементно или по полям. Для pointer-подобных или ссылкоподобных типов, таких как pointers, slices, maps, channels, functions и interfaces, нулевым значением является nil. Это важно, потому что переменные Go и пропущенные поля struct начинают жизнь в детерминированном состоянии, а не содержат мусорные значения, и многие API спроектированы так, что нулевое значение является полезным значением по умолчанию, хотя некоторые nil-значения всё же требуют инициализации перед определёнными операциями.
2Как Go обрабатывает равенство структур и что происходит, если структура содержит несравнимые поля?
Значения структур в Go можно сравнивать с помощью `==` и `!=` только когда каждое поле структуры comparable. Равенство сравнивает соответствующие поля по правилам равенства каждого поля. Если структура содержит неcomparable-поле, например срез, map или функцию, тип структуры не comparable, и сравнение двух значений такого типа через `==` — ошибка компиляции. Для таких структур используйте пользовательскую логику сравнения или подходящий помощник глубокого равенства, особенно в тестах.
3Объясните неизменяемость строк в Go и связь между string, []byte, bytes.Buffer и strings.Builder.
В Go `string` — это неизменяемая последовательность байтов, часто UTF-8 текст, но не обязательно валидный UTF-8. Строку нельзя изменить на месте; чтобы изменить содержимое, обычно конвертируют в `[]byte` для правок на уровне байтов или в `[]rune` для правок на уровне code point, а затем конвертируют обратно. Обычные преобразования между `string` и `[]byte` копируют данные и могут выделять память, поэтому повторные преобразования или повторная конкатенация в циклах могут быть дорогими. `strings.Builder` оптимизирован для эффективной сборки строк, а `bytes.Buffer` — изменяемый байтовый буфер, полезный для байтовых данных и I/O, и тоже может дать строку.
4Как работают указатели в Go и какие операции намеренно запрещены по сравнению с C?
Указатель в Go — это типизированное значение, которое хранит адрес другого значения, например *int указывает на int. Используйте &x, чтобы взять адрес значения, и *p, чтобы разыменовать указатель для чтения или записи значения, на которое он указывает. Передача или хранение указателя позволяет нескольким местам наблюдать или изменять одно и то же нижележащее значение, а указатель может быть nil. В отличие от C, безопасный Go намеренно запрещает арифметику указателей и произвольные манипуляции с «сырыми» адресами; низкоуровневые исключения требуют пакета unsafe.
5Объясните, как в Go работают константы и iota, включая typed и untyped constants.
Константы в Go — это значения времени компиляции: boolean, string или numeric. У typed constant есть конкретный тип. У untyped constant нет фиксированного конкретного типа, пока она не используется в типизированном контексте; numeric untyped constants до этого представлены точно/с высокой точностью; значение должно быть представимо в выбранном типе. `iota` — предописанный идентификатор в объявлениях `const`: он начинается с 0 в каждом блоке const и увеличивается для каждой спецификации константы, что удобно для enum-подобных констант и bit flags.
6Как в Go работают явные преобразования (explicit conversions) и почему язык избегает многих неявных преобразований?
Go в общем случае требует явных преобразований вида `T(x)` при смене типа значения, например `int64(i)` или `MyID(n)`. Это делает смену типа видимой и избегает неожиданных неявных numeric-, boolean- или string-преобразований. Преобразования допускаются только по правилам conversion rules Go и могут изменить значение, например из-за integer overflow/truncation, float-to-int truncation к нулю или потери точности. Untyped constants гибче: их можно использовать в типизированном контексте, если значение константы представимо в этом типе.
7Опишите разницу между массивами и срезами в Go, включая поведение length, capacity и нижележащего хранилища.
Массив в Go имеет фиксированную длину, которая является частью его типа, например [3]int; он хранит элементы непосредственно, а присваивание или передача массива копирует всё значение массива. Срез, например []int, — это небольшой дескриптор над нижележащим массивом: концептуально он содержит указатель на элементы, length и capacity. Length среза — число видимых элементов; capacity — сколько элементов можно использовать от начала среза до конца backing-массива. Срезы гибкие: reslicing меняет дескриптор, а append может повторно использовать тот же нижележащий массив, если позволяет capacity, или выделить новый, если нет.
8Как ведёт себя тип map в Go в отношении типов ключей, отсутствующих ключей, nil-map и порядка итерации?
Типы ключей map в Go должны быть comparable; срезы, map и функции нельзя напрямую использовать как ключи. Поиск ключа, которого нет, возвращает нулевое значение типа элемента, поэтому форму comma-ok (`v, ok := m[k]`) используют, чтобы отличить отсутствие от присутствующего нулевого значения. Из nil-map можно читать и по ней можно range, но запись в неё вызывает panic; перед записью её нужно инициализировать. Порядок итерации по map не специфицирован, и код не должен от него зависеть.
9Когда в Go следует передавать struct или другое значение по значению, а когда — по указателю?
Передавайте по значению, когда значение небольшое, только читается в рамках вызова или когда нужна независимая семантика копирования. Передавайте по указателю, когда функция должна изменять значение вызывающей стороны, когда копирование было бы дорогим, когда тип не следует копировать или когда nil/разделяемая идентичность являются частью API. Не предполагайте, что указатели всегда быстрее: они могут добавлять aliasing, heap escape, дополнительную работу для GC и менее cache-friendly доступ. Сначала выбирайте понятную семантику, а для чувствительных к производительности решений опирайтесь на benchmark-и.
10Как в Go ведут себя присваивание и копирование для structs, arrays, maps и slices?
Присваивание в Go копирует присваиваемое значение, но эффект зависит от типа. Присваивание struct копирует поля struct, а присваивание array копирует каждый элемент. Присваивание slice копирует заголовок slice — pointer, length и capacity — поэтому slices обычно разделяют один и тот же backing array. Присваивание map копирует ссылкоподобный дескриптор map, поэтому обе переменные ссылаются на одни и те же данные map. Для независимого владения нужно делать defensive copy, например с помощью copy или slices.Clone для slices, а для maps — создавать новую map и копировать в неё элементы.
11Как замыкания захватывают переменные в Go и какие ошибки могут возникать вокруг циклов и горутин?
Замыкание в Go — это значение-функция, которое ссылается на переменные из окружающей лексической области видимости. Оно захватывает сами переменные, поэтому замыкание может наблюдать последующие изменения, а захваченные переменные могут жить дольше создающей функции, если замыкание «убегает» (escapes). Ошибки вокруг циклов и горутин возникают, когда замыкания разделяют или наблюдают переменную после того, как она изменилась, из‑за чего горутины или отложенные колбэки часто видят не то значение или вызывают гонки данных на общем состоянии. Обычное исправление — передать нужное значение аргументом в замыкание или создать новую локальную копию на каждой итерации, а при необходимости синхронизировать конкурентный доступ. Начиная с Go 1.22 переменные цикла, объявленные через `for`/`range`, создаются на каждую итерацию, что устраняет многие классические баги захвата переменной цикла, но повторно используемые переменные вне цикла и другое общее изменяемое состояние по‑прежнему могут вызывать проблемы.
12Опишите семантику range по массивам, срезам, map, строкам и каналам.
`range` итерирует в зависимости от типа операнда. По массиву или срезу он даёт индекс и значение элемента; значение элемента — это копия, поэтому присваивание ему не изменяет коллекцию. Range по значению массива копирует массив для итерации, а range по срезу использует заголовок среза и индексирует нижележащий массив. По map он даёт ключ и значение в неопределённом порядке, и значение — копия. По строке он даёт байтовый индекс и декодированную Unicode code point (`rune`), а не индекс позиции rune. По каналу он получает значения, пока канал не закрыт и не опустошён; range по nil-каналу блокируется навсегда.
13В чём разница между value receiver и pointer receiver и как выбор receiver влияет на мутабельность, копирование и удовлетворение интерфейсов?
Метод с value receiver получает копию receiver, поэтому подходит для поведения только для чтения и небольших immutable-style типов; изменения копии receiver не видны вызывающему коду. Метод с pointer receiver получает копию указателя на receiver, поэтому может мутировать оригинал и избегать копирования больших или should-not-copy значений. Выбор receiver влияет на method sets: методы с value receiver входят в method set и T, и *T, а методы с pointer receiver — только в method set *T. Поэтому интерфейс, требующий метод с pointer receiver, удовлетворяется *T, а не T, даже хотя addressable значение T может вызывать метод обычным синтаксисом вызова метода.
14Опишите, как работают interfaces в Go и что означает неявное удовлетворение interface типом.
Interface в Go определяет набор обязательных методов. Конкретный тип неявно удовлетворяет interface, когда его method set содержит эти методы; никакого объявления implements не требуется. Переменная interface может хранить конкретное динамическое значение, тип которого удовлетворяет interface, и вызовы методов через interface диспетчеризуются к реализации этого динамического значения. Пустой interface, записываемый как interface{} или обычно any, не требует ни одного метода, поэтому ему удовлетворяет любой тип. Удовлетворяют ли interface типы T, *T или оба, зависит от их method sets и выбора receiver-ов.
15Как работают type assertions и type switches со значениями интерфейсов?
Type assertion проверяет динамическое значение, хранящееся в интерфейсе: `v := x.(T)` успешен, если динамический тип значения интерфейса — `T`, или если `T` — интерфейс, реализуемый динамическим значением. Форма с одним результатом вызывает panic при неудаче. Форма comma-ok, `v, ok := x.(T)`, сообщает об успехе без panic. Type switch, `switch v := x.(type)`, ветвится по динамическому типу значения интерфейса, причём `v` типизирован согласно совпавшему case.