Підготовка до співбесіди з Go

Питання для співбесіди з Go для Backend-розробників

Відібрані питання для співбесіди з Go для backend-розробників, згруповані за темами та сформовані з того ж каталогу питань, який використовується для практики в EngineerSpeak.

Почати AI-співбесіду з Go BackendБанківська картка не потрібна. Доступна 1 безкоштовна сесія.
Практика технічних співбесід англійськоюРежим для тих, для кого англійська не є рідною, щоб тренувати технічні співбесіди.

Система типів

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Поясніть, чим по-різному працює nil для вказівників, зрізів, мап, каналів, функцій та інтерфейсів у Go.

У Go nil — це нульове значення для вказівників, зрізів, мап, каналів, функцій та інтерфейсів, але операції з такими значеннями nil відрізняються залежно від типу. Вказівник nil можна порівняти з nil, але його розіменування викликає panic. Зріз nil має довжину та ємність 0; по ньому можна ітерувати через range і до нього можна додавати елементи через append. З мапи nil можна читати та ітерувати через range, але запис у неї викликає panic. Надсилання в канал nil або отримання з нього блокується назавжди, а закриття каналу nil викликає panic. Виклик функції nil викликає panic. Інтерфейс є nil лише тоді, коли в нього немає динамічного типу й динамічного значення; інтерфейс, що містить типізоване значення nil, наприклад вказівник nil, сам по собі не є nil.

Відповісти на це запитання з ШІ-тренером

5Що таке порівнювані типи в Go і як правила порівнюваності впливають на ключі map, перевірку на рівність та обмеження узагальнень (generics)?

Порівнювані типи в Go — це типи, значення яких можна порівнювати за допомогою `==` та `!=`. Базові типи, вказівники, канали, інтерфейси, а також структури й масиви, чиї поля або елементи є порівнюваними, також є порівнюваними; зрізи (slices), відображення (maps) та функції не є порівнюваними, за винятком порівняння з `nil`. Ключі `map` повинні бути порівнюваними. Перевірка на рівність підпорядковується правилам порівняння типу, а порівняння інтерфейсів залежить від динамічних конкретних значень; якщо порівнюваний інтерфейс містить непорівнюване динамічне значення, під час порівняння виникає паніка (panic). В узагальненнях наперед оголошене обмеження `comparable` дозволяє порівнювати параметри типу за допомогою `==`/`!=` та використовувати їх як ключі `map`.

Відповісти на це запитання з ШІ-тренером

6Як Go представляє байти, руни та текст у кодуванні UTF-8, і чому значення len(s) може відрізнятися від кількості символів, які бачить користувач?

У Go `byte` є псевдонімом для `uint8` і представляє один сирий байт, тоді як `rune` є псевдонімом для `int32` і представляє кодову точку (code point) Unicode. Рядок (`string`) — це послідовність байтів лише для читання, зазвичай це текст у кодуванні UTF-8, але він може містити будь-які довільні байти. Функція `len(s)` повертає кількість байтів, а не рун чи видимих користувачеві символів. Індексація рядка повертає байт; ітерація по рядку за допомогою `range` декодує UTF-8 і видає індекси байтів разом із рунами. Значення `len(s)` може відрізнятися від кількості видимих символів, оскільки UTF-8 може використовувати кілька байтів на одну кодову точку, а також тому, що один видимий користувачеві символ може складатися з кількох кодових точок, як-от комбіновані знаки (combining marks) або послідовності емодзі.

Відповісти на це запитання з ШІ-тренером

Структури даних

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Опишіть, як повторне взяття зрізу та присвоєння можуть призвести до того, що кілька зрізів спільно використовують один базовий масив, і які помилки це може спричинити.

Значення зрізу — це заголовок, що вказує всередину базового масиву. Присвоєння зрізу або передача його у функцію копіює лише цей заголовок, а не елементи. Повторне взяття зрізу створює інший заголовок, що вказує на діапазон того самого базового масиву. Тому кілька зрізів можуть посилатися на одне сховище: зміна елемента через один зріз може бути видима через інший, а append до одного зрізу може перезаписати дані, видимі іншому, якщо ще є запас ємності. Серед помилок — несподівані мутації, пошкоджені результати, утримання великих базових масивів через малі підзрізи та гонки даних, коли такі спільні посилання використовують одночасно. Щоб уникнути небажаного спільного використання, зробіть захисну копію за допомогою copy або append([]T(nil), s...), або обмежте ємність повним виразом зрізу перед append.

Відповісти на це запитання з ШІ-тренером

10Поясніть концептуально зростання зрізу під час виконання `append` та вплив повторних перерозподілів пам'яті на продуктивність.

Коли `append` додає елементи до зрізу, він записує їх у наявний базовий масив (backing array), якщо зріз має достатню місткість (capacity). Якщо місткості недостатньо, Go виділяє пам'ять для більшого базового масиву, копіює наявні елементи, записує нові елементи та повертає заголовок зрізу (slice header), який вказує на нове сховище. Точна політика зростання залежить від реалізації, але концептуально місткість зростає достатньо, щоб зробити повторні виклики `append` амортизовано ефективними. Повторні перерозподіли пам'яті все одно споживають ресурси процесора на копіювання, створюють додаткові виділення пам'яті, збільшують навантаження на GC (Garbage Collector) і можуть порушити спільне використання пам'яті зі старими псевдонімами зрізу. Якщо ви знаєте очікуваний розмір, попередньо виділяйте пам'ять за допомогою `make([]T, 0, n)` при формуванні через `append` або `make([]T, n)` при заповненні за індексом, щоб зменшити кількість перерозподілів.

Відповісти на це запитання з ШІ-тренером

Семантика мови

11Як працює порожній ідентифікатор (blank identifier) у Go для невикористаних значень, імпортів та перевірок інтерфейсів під час компіляції?

Порожній ідентифікатор `_` — це заповнювач (placeholder), доступний лише для запису. Присвоєння йому відкидає значення і не створює змінної, яку можна використовувати. Він застосовується для ігнорування непотрібних значень, що повертаються, або змінних циклу, для імпорту пакета лише заради побічних ефектів за допомогою `import _ "pkg"`, а також для перевірки реалізації інтерфейсу під час компіляції, наприклад, `var _ io.Reader = (*MyReader)(nil)`. Порожній імпорт (blank import) все одно запускає ініціалізацію імпортованого пакета. Присвоєння для перевірки інтерфейсу не скомпілюється, якщо `method set` конкретного типу не задовольняє цей інтерфейс.

Відповісти на це запитання з ШІ-тренером

Пакети

12Як працює порядок ініціалізації пакетів у Go, включаючи функції init та імпортовані залежності?

Go ініціалізує пакети в порядку залежностей. Імпортовані залежності пакета ініціалізуються до пакета, який їх імпортує. У межах пакета змінні на рівні пакета ініціалізуються до будь-яких функцій `init`, причому ініціалізація змінних впорядковується за залежностями та порядком оголошення, як визначено мовою. Після цього автоматично запускаються функції `init` пакета; пакет може мати кілька функцій `init`, і їх не можна викликати безпосередньо. Кожен пакет ініціалізується один раз. Для виконуваного файлу спочатку ініціалізується граф імпортів, потім ініціалізується пакет `main`, і, нарешті, викликається `main.main`.

Відповісти на це запитання з ШІ-тренером

13Поясніть правила видимості пакетів у Go, включаючи експортовані ідентифікатори та угоду щодо каталогу `internal/`.

У мові Go видимість пакетів контролюється іменуванням ідентифікаторів, а не ключовими словами доступу. Ідентифікатор, ім'я якого починається з великої літери Unicode, є експортованим (exported) і на нього можна посилатися з інших пакетів; інші ідентифікатори є неекспортованими і можуть використовуватися лише в межах того ж пакету. Це стосується функцій, типів, методів, змінних, констант і полів структур. Пакети використовують експортовані ідентифікатори для визначення свого публічного API (Application Programming Interface), залишаючи деталі реалізації неекспортованими. Крім того, пакет, розташований у каталозі `internal/`, може імпортуватися лише тим кодом, чий шлях імпорту знаходиться в батьківському дереві цього каталогу `internal`; це правило забезпечується інструментарієм (toolchain) Go.

Відповісти на це запитання з ШІ-тренером

Керування потоком виконання

14Як працює `defer` у Go, включаючи порядок виконання, час обчислення аргументів та взаємодію з поверненими значеннями?

`defer` планує виклик функції на момент завершення охоплюючої функції, незалежно від того, чи завершується вона звичайним поверненням (return), чи через розгортання стека при паніці (panic unwinding). Кілька відкладених викликів виконуються у порядку «останнім прийшов — першим вийшов» (LIFO). Значення відкладеної функції та її аргументи обчислюються негайно під час виконання інструкції `defer`, але сам виклик відбувається пізніше. У випадку з іменованими поверненими значеннями інструкція `return` спочатку присвоює ці значення, а вже потім виконуються відкладені функції, тому відкладене замикання (closure) може спостерігати або змінювати іменовані змінні результату ще до того, як їх отримає той, хто викликав функцію. Це робить `defer` корисним для очищення, наприклад, для закриття файлів, розблокування м'ютексів та звільнення ресурсів.

Відповісти на це запитання з ШІ-тренером

Обробка помилок

15Поясніть модель обробки помилок у Go та загальноприйняті способи їх створення, повернення і перевірки.

Go обробляє помилки як звичайні значення, а не як винятки (exceptions). Вбудований інтерфейс `error` реалізується будь-яким типом, що має метод `Error() string`. Функції зазвичай повертають `error` як останній результат, де `nil` означає успішне виконання, а відмінне від `nil` значення помилки означає, що функція, яка викликала (caller), повинна обробити або передати збій далі. Прості помилки зазвичай створюються за допомогою `errors.New`, відформатовані помилки — за допомогою `fmt.Errorf`, а код, що викликає, зазвичай використовує перевірку `if err != nil { ... }`.

Відповісти на це запитання з ШІ-тренером

16Як слід обробляти відновлення після паніки (panic recovery) у бекенд-сервісах на Go, зокрема що відбувається, коли в goroutine виникає паніка, і коли процес повинен відновитися, а коли — завершитися аварійно (crash)?

Паніка розгортає стек поточної `goroutine`, виконуючи її відкладені (deferred) функції. Функція `recover` працює лише тоді, коли вона викликана з відкладеної функції в тій самій `goroutine`; одна `goroutine` не може перехопити паніку іншої. Якщо паніку не перехоплено, процес завершується аварійно. У бекенд-сервісах відновлення зазвичай слід розміщувати на межах ізоляції, таких як обробники запитів, проміжне програмне забезпечення (middleware) RPC (Remote Procedure Call) або точки входу робочих `goroutine`, щоб один невдалий запит або завдання не призвели до падіння всього сервісу. Але якщо паніка могла пошкодити спільний стан (shared state) або зробити цілісність процесу ненадійною, безпечніше дозволити процесу впасти та перезапуститися, ніж перехоплювати помилку і наосліп продовжувати роботу.

Відповісти на це запитання з ШІ-тренером

Конкурентність

17Що таке канали зі значенням `nil` в Go, і як вони можуть випадково зламати код або навмисно відключити гілки `select`?

Канал зі значенням `nil` — це змінна каналу, значення якої дорівнює `nil`, часто через те, що вона не була ініціалізована за допомогою `make` або була явно встановлена в `nil`. Відправлення даних у канал `nil` або їх отримання з нього призводить до вічного блокування. У конструкції `select` гілка (case) з каналом `nil` ніколи не буває готовою до виконання, тому присвоєння змінній каналу значення `nil` може навмисно відключити цю гілку. Випадкове використання каналу `nil` може призвести до зависання `goroutine` або до того, що логіка `select` перестане обробляти очікувані події.

Відповісти на це запитання з ШІ-тренером

18Чим атомарні операції в sync/atomic відрізняються від синхронізації на основі м'ютексів, і коли їх доречно використовувати?

sync/atomic забезпечує неподільні операції над окремими ділянками пам'яті, такі як load, store, add, swap та compare-and-swap, з гарантіями синхронізації та впорядкування пам'яті (memory-ordering). М'ютекс захищає критичну секцію, тому він може охороняти довільний код та інваріанти, що включають кілька операцій читання, запису або полів. Атомарні операції підходять для простого незалежного стану, такого як лічильники, прапорці, порядкові номери або ретельно спроєктовані структури без блокувань (lock-free). Надавайте перевагу м'ютексу, коли операції є складеними, кілька значень повинні залишатися узгодженими, або коли атомарну версію було б складно осмислити чи довести її правильність.

Відповісти на це запитання з ШІ-тренером

19Як слід проєктувати володіння каналами та час життя горутин, щоб уникнути витоків горутин?

Проєктуйте горутини з явним власником, чітким сигналом завершення роботи та гарантованим шляхом виходу. Сторона-виробник (producer) зазвичай володіє закриттям каналу, особливо вихідного каналу; одержувачі (receivers) не повинні закривати канал, поки відправники (senders) можуть бути ще активними. Кожне блокувальне відправлення, отримання, цикл, таймер або зовнішній виклик повинні або гарантовано завершуватися, або мати можливість розблокуватися під час скасування, зазвичай через context.Context або канал done. Використовуйте WaitGroup, errgroup або подібні засоби координації, щоб дочекатися завершення робочих горутин-обробників (workers), а канали закривалися лише після того, як відправники завершать роботу.

Відповісти на це запитання з ШІ-тренером

20Які поширені причини витоку goroutine у сервісах на Go, і як виявляти та виправляти їх у продакшені?

Поширені витоки `goroutine` у Go-сервісах виникають через те, що `goroutine` назавжди блокуються на надсиланні або отриманні з каналу (channel), очікують на інші блокувальні операції без можливості скасування (cancellation), застрягають на операціях вводу/виводу без тайм-аутів (deadlines), виконують фонові цикли або таймери (tickers), які ніколи не зупиняються, а також через `goroutine` рівня запиту, які продовжують жити після завершення запиту. У продакшені слід шукати стійке зростання кількості `goroutine` та пов'язані з цим симптоми, а потім інспектувати дампи `goroutine` або профілі `goroutine` в `pprof`, щоб побачити, де саме вони застрягли. Виправлення витоку означає зміну коду таким чином, щоб ці `goroutine` могли завершитись: додавання скасування та тайм-аутів, зупинка таймерів, коректне закриття каналів, уникнення відкріплених (detached) `goroutine` для запитів та обмеження рівня конкурентності, де це необхідно.

Відповісти на це запитання з ШІ-тренером