1Поясніть, як у Go працюють нульові значення для вбудованих типів і типів, подібних до посилань, і чому це важливо під час оголошення змінних без явної ініціалізації.
У Go змінна, оголошена без явного ініціалізатора, автоматично ініціалізується нульовим значенням свого типу. Числові типи стають 0, bool — false, string — "", а масиви та структури обнуляються елемент за елементом або поле за полем. Типи, подібні до вказівників або посилань, — вказівники, зрізи, мапи, канали, функції та інтерфейси — мають як нульове значення nil. Це важливо, бо змінні Go та пропущені поля структур починають існування в детермінованому стані, а не містять «сміття», і багато API (Application Programming Interface) спроєктовані так, що нульове значення є корисним типовим значенням за замовчуванням; щоправда, деякі значення nil усе одно потребують ініціалізації перед певними операціями.
2Як Go обробляє перевірку на рівність для структур і що відбувається, коли структура містить непорівнювані поля?
Значення структур у Go можна порівнювати за допомогою `==` та `!=` лише тоді, коли кожне поле в структурі є порівнюваним. Перевірка на рівність порівнює відповідні поля, використовуючи власне правило порівняння кожного поля. Якщо структура містить непорівнюване поле, як-от зріз (slice), map або функцію, тип структури стає непорівнюваним, і порівняння двох значень такого типу структури за допомогою `==` призведе до помилки під час компіляції. Для таких структур слід використовувати власну логіку порівняння або відповідну допоміжну функцію для глибокого порівняння (deep-equality), особливо в тестах.
3Поясніть незмінність (immutability) рядків у Go та зв'язок між string, []byte, bytes.Buffer та strings.Builder.
Рядок (`string`) у Go — це незмінна послідовність байтів, часто текст у кодуванні UTF-8, але він не обов'язково має бути валідним UTF-8. Ви не можете змінити рядок на місці; щоб змінити вміст, ви зазвичай перетворюєте його на `[]byte` для редагування на рівні байтів або `[]rune` для редагування на рівні кодових точок (code points), а потім перетворюєте назад. Звичайні перетворення між `string` та `[]byte` копіюють дані та можуть виділяти пам'ять, тому багаторазові перетворення або багаторазова конкатенація в циклах можуть бути ресурсомісткими. `strings.Builder` оптимізовано для ефективного формування рядків, тоді як `bytes.Buffer` — це змінний буфер байтів, корисний для байт-орієнтованих даних та операцій вводу-виводу, який також може генерувати рядок.
4Як працюють вказівники у Go, і які операції навмисно заборонені порівняно з C?
Вказівник у Go — це типізоване значення, яке містить адресу іншого значення, наприклад `*int`, що вказує на `int`. Використовуйте `&x`, щоб отримати адресу значення, та `*p` для розіменування вказівника, щоб прочитати або записати значення, на яке він вказує. Передавання або збереження вказівника дозволяє з різних місць програми спостерігати або змінювати одне й те саме базове значення, причому вказівник може дорівнювати `nil`. На відміну від C, безпечний Go навмисно забороняє адресну арифметику та довільні маніпуляції з 'сирими' адресами (raw-address manipulation); для низькорівневих винятків потрібен пакет `unsafe`.
5Поясніть, як працюють константи та `iota` в Go, зокрема типізовані та нетипізовані константи.
Константи в Go — це значення часу компіляції: логічні, рядкові або числові. Типізована константа має конкретний тип. Нетипізована константа не має фіксованого конкретного типу, поки її не використають у типізованому контексті, а числові нетипізовані константи до цього моменту подаються точно/з високою точністю; значення має бути придатним для представлення в обраному типі. `iota` — це наперед оголошений ідентифікатор, який використовується в оголошеннях `const`: він починається з 0 у кожному блоці `const` і збільшується на одиницю для кожної специфікації константи, що робить його зручним для констант, подібних до перелічень, і бітових прапорців.
6Як працюють явні перетворення типів у Go, і чому мова уникає багатьох неявних перетворень?
Зазвичай Go вимагає явних перетворень за допомогою `T(x)` під час зміни типу значення, наприклад `int64(i)` або `MyID(n)`. Це робить зміни типів видимими та дозволяє уникнути несподіваних неявних числових, логічних або рядкових перетворень. Перетворення дозволені лише за правилами перетворення Go і можуть змінювати значення, наприклад, через переповнення або усічення цілих чисел, усічення чисел із рухомою комою до цілих у напрямку до нуля або втрату точності. Нетипізовані константи є гнучкішими: їх можна використовувати в типізованому контексті, якщо значення константи може бути представлено в цьому типі.
7Опишіть різницю між масивами та зрізами в Go, зокрема як поводяться довжина, ємність і базове сховище.
Масив у Go має фіксовану довжину, яка є частиною його типу, наприклад [3]int; він зберігає елементи безпосередньо, а присвоєння або передача масиву копіює все значення масиву. Зріз, наприклад []int, — це невеликий дескриптор над базовим масивом: концептуально він містить вказівник на елементи, довжину та ємність. Довжина зрізу — це кількість видимих елементів; його ємність — скільки елементів можна використати від початку зрізу, перш ніж досягти кінця базового масиву. Зрізи гнучкі: повторне взяття зрізу змінює дескриптор, а append може повторно використати той самий базовий масив, якщо дозволяє ємність, або виділити новий, якщо ні.
8Як тип `map` у Go поводиться щодо типів ключів, відсутніх ключів, `map` зі значенням `nil` та порядку ітерації?
Типи ключів для `map` у Go мають бути порівнюваними (comparable); зрізи (slices), `map` та функції не можна використовувати безпосередньо як ключі. Пошук відсутнього ключа повертає нульове значення (zero value) типу елемента, тому для розрізнення відсутності та наявного нульового значення використовується форма comma-ok (`v, ok := m[k]`). З `map` зі значенням `nil` можна зчитувати дані та ітеруватися по ній, але присвоєння викликає паніку (panic); ініціалізуйте її перед записом. Порядок ітерації по `map` не визначений, і код не повинен залежати від нього.
9Коли слід передавати структуру або інше значення за значенням, а коли — за покажчиком у Go?
Передавайте за значенням, коли значення є невеликим, доступним лише для читання для виклику, або коли вам потрібна незалежна семантика копіювання. Передавайте за покажчиком, коли функція повинна змінити значення того, хто її викликає, копіювання буде ресурсомістким, тип не слід копіювати, або значення `nil`/спільна ідентичність (shared identity) є частиною API. Не припускайте, що покажчики завжди швидші: вони можуть додавати псевдоніми (aliasing), витоки в купу (heap escapes), роботу для GC (Garbage Collector) і менш дружній до кешу доступ. Надавайте перевагу чіткій семантиці в першу чергу та проводьте бенчмаркінг для рішень, що є критичними до продуктивності.
10Як працюють присвоєння та копіювання для структур, масивів, відображень (maps) та зрізів (slices) у Go?
Присвоєння в Go копіює значення, що присвоюється, але ефект залежить від типу. Присвоєння структури копіює поля структури, а присвоєння масиву копіює кожен елемент. Присвоєння зрізу копіює заголовок зрізу (вказівник, довжину та місткість), тому зрізи зазвичай спільно використовують один базовий масив (backing array). Присвоєння відображення копіює дескриптор, що діє як посилання, тому обидві змінні посилаються на одні й ті самі дані відображення. Для забезпечення незалежного володіння створюйте захисну копію (defensive copy), наприклад, використовуючи copy або slices.Clone для зрізів, або створюючи нове відображення і копіюючи туди записи для відображень.
11Як замикання захоплюють змінні в Go, і які помилки можуть виникати при роботі з циклами та горутинами?
Замикання в Go — це значення функції, яке посилається на змінні з її навколишньої лексичної області видимості. Воно захоплює самі змінні, тому замикання може спостерігати зміни їхніх значень, а захоплені змінні можуть мати більший час життя, ніж функція, що їх створила, якщо замикання виходить за межі локальної видимості (escapes). Помилки навколо циклів та горутин трапляються, коли замикання спільно використовують або спостерігають змінну після її зміни. Це часто призводить до того, що горутини або відкладені зворотні виклики (callbacks) бачать небажане значення, або виникає стан гонитви (data race) за спільним станом. Типовим виправленням є передача потрібного значення як аргументу в замикання або створення нової локальної копії для кожної ітерації, а також синхронізація конкурентного доступу за потреби. Починаючи з Go 1.22, змінні циклу, оголошені через `for`/`range`, існують у межах однієї ітерації. Це усуває багато класичних помилок із захопленням змінних циклу, але перевикористані змінні поза циклом та інший спільний змінюваний (mutable) стан усе ще можуть викликати проблеми.
12Опишіть семантику використання `range` з масивами, зрізами (slices), мапами (maps), рядками та каналами.
`range` ітерує залежно від типу операнда. При ітерації по масиву або зрізу він повертає індекс та значення елемента; значення елемента є копією, тому його зміна не впливає на саму колекцію. Ітерація по значенню масиву копіює весь масив для перебору, тоді як ітерація по зрізу використовує заголовок зрізу та індексує базовий масив. При ітерації по мапі він повертає ключ і значення в невизначеному порядку, і це значення також є копією. При ітерації по рядку він повертає індекс байта та розкодовану кодову точку Unicode (`rune`), а не порядковий індекс руни. При ітерації по каналу він отримує значення доти, доки канал не буде закрито та вичерпано; ітерація по `nil` каналу блокується назавжди.
13У чому різниця між методами з одержувачем за значенням (value receivers) та одержувачем за вказівником (pointer receivers), і як вибір одержувача впливає на змінність (mutability), копіювання та задоволення інтерфейсів?
Метод з одержувачем за значенням отримує копію одержувача, тому він підходить для поведінки лише для читання та невеликих типів у незмінному стилі; зміни в копії одержувача не видимі для того, хто викликає метод. Метод з одержувачем за вказівником отримує копію вказівника на одержувача, тому він може змінювати оригінал і уникати копіювання великих значень або таких, що не слід копіювати. Вибір одержувача впливає на `method set`: методи з одержувачем за значенням входять до `method set` як `T`, так і `*T`, тоді як методи з одержувачем за вказівником входять лише до `method set` для `*T`. Відповідно, інтерфейс, який вимагає методу з одержувачем за вказівником, задовольняється типом `*T`, а не `T`, навіть якщо значення `T`, що має адресу в пам'яті (addressable), може викликати цей метод за допомогою звичайного синтаксису виклику методів.
14Опишіть, як працюють інтерфейси у Go та що означає неявна реалізація інтерфейсу типом.
Інтерфейс у Go визначає набір обов'язкових методів. Конкретний тип реалізує інтерфейс неявно, якщо його набір методів (method set) містить ці методи; тут немає декларації `implements`. Змінна типу інтерфейсу може зберігати конкретне динамічне значення, тип якого реалізує цей інтерфейс, а виклики методів інтерфейсу перенаправляються (dispatch) до реалізації цього динамічного значення. Порожній інтерфейс, який записується як `interface{}` або зазвичай `any`, не має обов'язкових методів, тому його реалізує будь-який тип. Чи реалізує інтерфейс тип `T`, `*T`, або обидва — залежить від їхніх наборів методів і вибору одержувача (receiver).
15Як працюють твердження типу (type assertions) та перемикачі типу (type switches) зі значеннями інтерфейсів?
Твердження типу (type assertion) перевіряє динамічне значення, що зберігається в інтерфейсі: `v := x.(T)` виконується успішно, якщо динамічний тип значення інтерфейсу є `T`, або якщо `T` є інтерфейсом, реалізованим динамічним значенням. Форма з одним результатом викликає паніку (panic) у разі невдачі. Форма 'comma-ok', `v, ok := x.(T)`, повідомляє про успіх без паніки. Перемикач типу (type switch), `switch v := x.(type)`, виконує розгалуження на основі динамічного типу значення інтерфейсу, причому `v` отримує тип відповідно до збігу з варіантом (case).