1Объясните, чем поведение nil отличается для указателей, срезов, map, каналов, функций и интерфейсов в Go.
В Go nil — это нулевое значение для указателей, срезов, map, каналов, функций и интерфейсов, но операции с такими nil-значениями зависят от типа. Nil-указатель можно сравнить с nil, но разыменование вызывает panic. У nil-среза length и capacity равны 0; по нему можно итерироваться и к нему можно делать append. Из nil-map можно читать и по ней можно range, но запись в неё вызывает panic. Отправка в nil-канал или получение из него блокируется навсегда, а закрытие nil-канала вызывает panic. Вызов nil-функции вызывает panic. Интерфейс равен nil только когда у него нет ни динамического типа, ни динамического значения; интерфейс, содержащий типизированное nil-значение (например, nil-указатель), сам по себе не равен nil.
2Что такое comparable-типы в Go и как правила сравнимости влияют на ключи map, равенство и ограничения в дженериках?
Comparable-типы в Go — это типы, значения которых можно сравнивать с помощью `==` и `!=`. Базовые типы, указатели, каналы, интерфейсы, а также структуры/массивы, у которых все поля или элементы comparable, являются comparable; срезы, map и функции не comparable, за исключением сравнения с `nil`. Ключи map должны быть comparable. Равенство следует правилам сравнения типа, а сравнение интерфейсов зависит от динамических конкретных значений; если сравниваемый интерфейс содержит неcomparable динамическое значение, сравнение вызывает panic. В дженериках предопределённое ограничение `comparable` позволяет параметрам типа сравниваться через `==`/`!=` и использоваться как ключи map.
3Как Go представляет байты, руны и UTF-8 текст и почему len(s) может отличаться от числа видимых пользователю символов?
В Go `byte` — это псевдоним `uint8` и обозначает один «сырой» байт, а `rune` — псевдоним `int32` и обозначает Unicode code point. `string` — это read-only последовательность байтов, обычно UTF-8 текст, но может содержать произвольные байты. `len(s)` возвращает число байтов, а не рун или видимых пользователю символов. Индексация строки возвращает байт; range по строке декодирует UTF-8 и даёт байтовые индексы плюс руны. `len(s)` может отличаться от числа видимых символов, потому что UTF-8 может использовать несколько байтов на code point и потому что один видимый пользователю символ может состоять из нескольких code point, например combining marks или emoji-последовательности.
4Чем псевдонимы типов (type aliases) отличаются от определённых типов (defined types), и когда что использовать?
Определённый тип, например `type UserID int64`, создаёт новый отдельный тип с underlying type `int64`. Он не свободно присваивается к `int64` без преобразования и может иметь собственные методы. Псевдоним типа, например `type UserID = int64`, — это просто другое имя того же типа, поэтому тождество типов и присваиваемость сохраняются. Используйте определённые типы для предметного моделирования, type safety и методов; псевдонимы — в основном для рефакторинга, миграции или совместимости без введения нового типа.
5Как Go обрабатывает специальные значения чисел с плавающей точкой и какие ловушки сравнения важны в backend-системах?
В Go `float32` и `float64` используют поведение в стиле IEEE-754, включая специальные значения вроде плюс/минус infinity и NaN. Для `float64` есть помощники `math.Inf`, `math.IsInf`, `math.NaN` и `math.IsNaN`. NaN не равен ничему, включая себя, поэтому `x == x` ложно, когда `x` — NaN. Точное равенство вычисленных float также рискованно, потому что округление и точность могут сделать математически равные значения различными; используйте допуски, подходящие для предметной области, или избегайте float для точных бизнес-значений вроде денег. Floats допустимы как ключи map, но ключи NaN проблематичны, потому что поиск в map зависит от равенства, а NaN не сравнивается как равный даже самому себе.
6Опишите embedding структур в Go и как ведут себя promoted fields и methods.
Struct embedding в Go означает объявление поля по его типу без явного имени поля, например `type User struct { Person }`. Встроенное значение всё равно является реальным полем, доступным как `u.Person`, но его экспортируемые/доступные поля и методы могут быть promoted, так что вызывающий код может писать селекторы вроде `u.Name` или `u.Greet()` как сокращение для доступа через встроенное поле. Embedding — это композиция, а не классическое наследование: внешний тип автоматически не является subtype встроенного типа. Если promoted-селекторы конфликтуют, Go не угадывает; неоднозначные имена нужно квалифицировать, иначе они не выбираются через внешнее значение.
7Опишите, как reslicing и присваивание срезов могут привести к тому, что несколько срезов разделяют один и тот же нижележащий массив, и какие баги это может создать.
Значение среза — это заголовок, указывающий внутрь нижележащего массива. Присваивание среза или передача его в функцию копирует только этот заголовок, а не элементы. Reslicing создаёт другой заголовок, указывающий на диапазон того же backing-массива. Поэтому несколько срезов могут быть алиасами одного хранилища: изменение элемента через один срез может быть видно через другой, а append к одному срезу может перезаписать данные, видимые другому, если ещё есть запас capacity. Баги включают неожиданные мутации, испорченные результаты, удержание больших backing-массивов через маленькие подсрезы и data race при конкурентном использовании алиасов. Чтобы избежать непреднамеренного разделения, сделайте защитную копию через copy или append([]T(nil), s...), либо ограничьте capacity полным выражением среза перед append.
8Объясните рост среза при append на концептуальном уровне и влияние на производительность повторных перевыделений.
Когда append добавляет элементы в срез, он записывает их в существующий backing-массив, если у среза достаточно capacity. Если capacity недостаточна, Go выделяет больший backing-массив, копирует существующие элементы, записывает новые и возвращает заголовок среза, указывающий на новое хранилище. Точная политика роста зависит от реализации, но концептуально capacity растёт достаточно, чтобы повторные append были амортизированно эффективны. Повторные перевыделения всё равно стоят CPU на копирование, создают аллокации, увеличивают давление на GC и могут разорвать разделение со старыми алиасами срезов. Если ожидаемый размер известен, предварительно выделяйте с make([]T, 0, n) при построении через append или make([]T, n) при заполнении по индексу, чтобы уменьшить перевыделения.
9Что такое паттерны регистрации во время init в Go и какие риски несут side effects от blank-import и глобальные реестры?
Паттерн регистрации во время init — это когда пакет регистрирует реализацию в общем реестре из функции `init`. Blank import вида `_ "example.com/driver"` часто используют, чтобы импортировать пакет только ради side effects: при этом выполняется его `init`, хотя на экспортируемые имена никто не ссылается. Так делают для драйверов, кодеков, плагинов, метрик или точек расширения сериализации. Риски: скрытые зависимости и side effects при старте, глобальное изменяемое состояние, дублирующая или зависящая от порядка регистрация, более сложная изоляция в тестах и менее явная проводка зависимостей. Паттерн нужно применять осознанно, чётко документировать и часто смягчать явной регистрацией, идемпотентными/concurrency-safe реестрами либо инжектируемыми/сбрасываемыми реестрами для тестов.
10Как взаимодействуют defer, panic и именованные возвращаемые значения при реализации cleanup, который может изменять возвращаемые ошибки?
Отложенные функции выполняются после того, как возвращаемые значения уже присвоены, но до возврата функции вызывающему коду. Поэтому отложенное замыкание может читать или изменять именованные возвращаемые значения, например именованный `err`. Это обычно используют, чтобы добавить ошибки cleanup от `Close`, `Commit` или похожих операций к возвращаемой ошибке, ideally сохраняя первичную ошибку, а не перезаписывая её. Во время раскрутки panic отложенные функции всё равно выполняются; отложенная функция может сделать recover и установить именованное возвращаемое значение, но это следует ограничивать намеренными границами panic. Будьте осторожны и не затеняйте именованную возвращаемую переменную вроде `err`, потому что defer тогда может наблюдать или изменять другую переменную, чем предполагалось.
11Как работают errors.Is, errors.As и %w в цепочках оборачивания ошибок в Go?
`fmt.Errorf` с `%w` создаёт новую ошибку, которая оборачивает исходную и добавляет контекст. Обёртки открывают исходные ошибки через `Unwrap`, образуя цепочку или дерево, которое стандартная библиотека может инспектировать. `errors.Is(err, target)` проверяет, совпадает ли `err` или что-то из обёрнутого ею с целевой ошибкой. `errors.As(err, &target)` проверяет, можно ли присвоить `err` или что-то из обёрнутого ею целевому типу, и сохраняет найденное значение в переданный указатель.
12Что такое sentinel errors и каковы компромиссы по сравнению с пользовательскими типизированными ошибками или более богатыми доменными моделями ошибок?
Sentinel error — это именованное значение ошибки, часто package-level переменная вроде `var ErrNotFound = errors.New("not found")`, представляющее конкретное условие, которое вызывающие могут проверять, обычно через `errors.Is`, когда возможно оборачивание. Sentinel'ы просты и полезны для широких стабильных категорий, но экспортируемые sentinel'ы становятся частью API и могут связывать вызывающих с конкретными значениями. Пользовательские типизированные ошибки могут нести структурированные поля и находиться через `errors.As`. Более богатые доменные модели ошибок классифицируют сбои по kind/code и могут включать безопасные сообщения или метаданные — это полезно, когда вызывающим нужно стабильное поведение шире одного фиксированного значения ошибки.
13Как Go-бэкенду следует отображать внутренние ошибки в полезные ответы клиенту, при этом давая операторам диагностируемые сигналы?
Go-бэкенд должен преобразовывать внутренние ошибки на границе приложения или транспорта в стабильные, безопасные для клиента категории и ответы. Эти категории должны соответствовать подходящим HTTP status codes или эквивалентным транспортным статусам, с безопасными сообщениями и машиночитаемыми кодами, а не сырыми внутренними ошибками. Операторы при этом должны получать диагностическую информацию через структурированные логи, traces, metrics, correlation/request IDs и сохранённые исходные причины. Логирование обычно лучше выполнять один раз на границе, где есть request context, чтобы избежать и пропущенных сбоев, и шумных дублирующих логов.
14Как правильно реализовывать пользовательские типы ошибок в Go и как errors.Join влияет на inspection и обработку ошибок на пути cleanup?
Пользовательский тип ошибки в Go удовлетворяет `error`, реализуя `Error() string`. Он также может нести структурированные поля и реализовывать `Unwrap() error`, чтобы открывать исходную причину. Важны pointer versus value receivers: pointer receiver означает, что `error` удовлетворяет только `*T`, а value receiver обычно означает, что и `T`, и `*T` — это влияет на копирование и на то, какой тип вызывающий код должен использовать с `errors.As`. `errors.Join` объединяет несколько ошибок в одну; `errors.Is` и `errors.As` проверяют объединённые дочерние ошибки. Это полезно, когда нужно вернуть и основную ошибку операции, и ошибку cleanup/deferred, не теряя ни одного сбоя.
15Как работает select с channels, включая выбор ready-case, default cases и отменяемые операции?
select ожидает нескольких channel-операций и выполняет один case, чей send или receive может продолжиться. Если ни один channel case не готов, select блокируется, если только нет default case; default выполняется сразу только когда ни одна channel-операция не может продолжиться — это полезно для non-blocking попыток send/receive. Если готовы несколько cases, Go выбирает один псевдослучайно, а не по порядку в исходнике. Для отменяемых channel-операций обычно добавляют case с receive из ctx.Done(), чтобы goroutine могла перестать ждать при отмене context или timeout.