Příprava na pohovor z Go

Otázky na pohovor pro backendové vývojáře v Go

Vybrané otázky na pohovor z Go pro backendové vývojáře, seskupené podle témat a generované ze stejného katalogu otázek, který pohání procvičování na EngineerSpeak.

Spustit backendový AI pohovor z GoKreditní karta není potřeba. K dispozici je 1 bezplatná lekce.
Procvičování technických pohovorů v angličtiněRežim pro lidi, pro které angličtina není rodným jazykem, určený k procvičování technických pohovorů.

Typový systém

1Vysvětlete, jak fungují nulové hodnoty (zero values) v jazyce Go pro vestavěné a referenční typy a proč jsou důležité při deklaraci proměnných bez explicitní inicializace.

V jazyce Go je proměnná deklarovaná bez explicitního inicializátoru automaticky inicializována na nulovou hodnotu (zero value) svého typu. Číselné typy se nastaví na 0, bool na false, string na "" a pole nebo struktury (structs) jsou vynulovány prvek po prvku nebo položka po položce. Typy fungující jako ukazatele nebo reference, jako jsou ukazatele, řezy (slices), mapy, kanály (channels), funkce a rozhraní (interfaces), mají jako svou nulovou hodnotu nil. Je to důležité, protože proměnné v Go a vynechané položky struktur začínají v deterministickém stavu, místo aby obsahovaly nesmyslná data v paměti (garbage). Mnoho API (Application Programming Interfaces) je navíc navrženo tak, že nulová hodnota představuje užitečnou výchozí hodnotu, ačkoli některé hodnoty nil stále vyžadují před určitými operacemi dodatečnou inicializaci.

Zkusit odpovědět na tuto otázku s trenérem AI

2Jak Go řeší rovnost u struktur a co se stane, když struktura obsahuje neporovnatelné položky?

Hodnoty struktur v Go lze porovnávat operátory `==` a `!=` pouze tehdy, když je každá položka struktury porovnatelná. Rovnost porovnává odpovídající položky pomocí jejich vlastních pravidel pro rovnost. Pokud struktura obsahuje neporovnatelnou položku, jako je řez (slice), mapa nebo funkce, typ struktury není porovnatelný a porovnání dvou hodnot tohoto typu operátorem `==` vede k chybě při překladu (compile-time error). Pro takové struktury použijte vlastní logiku porovnávání nebo vhodnou pomocnou funkci pro hluboké porovnání (deep-equality), zejména v testech.

Zkusit odpovědět na tuto otázku s trenérem AI

3Vysvětlete neměnnost (immutability) řetězců v Go a vztah mezi `string`, `[]byte`, `bytes.Buffer` a `strings.Builder`.

Řetězec (`string`) v Go je neměnná sekvence bajtů, často text v kódování UTF-8, i když to není podmínkou. Řetězec nelze měnit na místě (in-place); pro změnu obsahu jej obvykle převedete na `[]byte` pro úpravy na úrovni bajtů nebo na `[]rune` pro úpravy kódových bodů (code points) a následně jej převedete zpět. Běžné převody mezi `string` a `[]byte` kopírují data a mohou alokovat paměť, takže opakované převody nebo zřetězování ve smyčkách mohou být výkonnostně náročné. Typ `strings.Builder` je optimalizován pro efektivní sestavování řetězců, zatímco `bytes.Buffer` je měnitelný bajtový buffer vhodný pro bajtově orientovaná data a I/O operace, který také dokáže vygenerovat řetězec.

Zkusit odpovědět na tuto otázku s trenérem AI

4Vysvětlete, jak v jazyce Go funguje hodnota nil odlišně pro ukazatele, řezy (slices), mapy, kanály (channels), funkce a rozhraní (interfaces).

V jazyce Go je nil nulovou hodnotou (zero value) pro ukazatele, řezy (slices), mapy, kanály (channels), funkce a rozhraní (interfaces), ale operace nad těmito hodnotami nil se liší podle typu. Ukazatel na nil lze s nil porovnávat, ale jeho dereference vyvolá paniku (panic). Řez s hodnotou nil má délku i kapacitu 0, lze přes něj iterovat a přidávat do něj prvky. Z mapy s hodnotou nil lze číst a iterovat přes ni, ale přiřazení do ní způsobí paniku. Odesílání do kanálu s hodnotou nil nebo přijímání z něj blokuje donekonečna a uzavření takového kanálu vyvolá paniku. Volání funkce s hodnotou nil rovněž způsobí paniku. Rozhraní (interface) je nil pouze tehdy, když nemá žádný dynamický typ ani dynamickou hodnotu; rozhraní obsahující typovou hodnotu nil, například nil ukazatel, samo o sobě nil není.

Zkusit odpovědět na tuto otázku s trenérem AI

5Co jsou porovnatelné typy v Go a jak pravidla pro porovnatelnost ovlivňují klíče map, rovnost a omezení generických typů (generics constraints)?

Porovnatelné typy v Go jsou typy, jejichž hodnoty lze porovnávat operátory `==` a `!=`. Základní typy, ukazatele, kanály, rozhraní (interfaces) a struktury nebo pole, jejichž položky či prvky jsou porovnatelné, jsou rovněž porovnatelné; řezy (slices), mapy a funkce porovnatelné nejsou, s výjimkou porovnání s `nil`. Klíče map musí být porovnatelné. Rovnost se řídí pravidly porovnávání daného typu a porovnání rozhraní závisí na dynamických konkrétních hodnotách; pokud porovnávané rozhraní obsahuje neporovnatelnou dynamickou hodnotu, porovnání vyvolá výjimku panic. V generikách umožňuje předdefinované omezení (constraint) `comparable` porovnávat parametry typu pomocí `==`/`!=` a používat je jako klíče v mapách.

Zkusit odpovědět na tuto otázku s trenérem AI

6Jak v Go reprezentujeme bajty, runy (runes) a text v kódování UTF-8, a proč se `len(s)` může lišit od počtu viditelných znaků pro uživatele?

V Go je `byte` alias pro `uint8` a představuje jeden surový bajt, zatímco `rune` je alias pro `int32` a představuje kódový bod (code point) v Unicode. Řetězec (`string`) je sekvence bajtů určená pouze pro čtení, obvykle obsahující text v kódování UTF-8, ale může obsahovat libovolné bajty. Funkce `len(s)` vrací počet bajtů, nikoliv run nebo znaků viditelných pro uživatele. Indexování řetězce vrací bajt; iterace přes řetězec dekóduje UTF-8 a vrací indexy bajtů a příslušné runy. Hodnota `len(s)` se může lišit od počtu viditelných znaků, protože UTF-8 může na jeden kódový bod použít více bajtů, a také proto, že jeden znak viditelný pro uživatele se může skládat z několika kódových bodů, jako jsou například kombinující značky (combining marks) nebo sekvence emotikonů (emoji).

Zkusit odpovědět na tuto otázku s trenérem AI

Datové struktury

7Popište rozdíl mezi poli (arrays) a řezy (slices) v Go, včetně toho, jak se chová jejich délka, kapacita a podkladové úložiště.

Pole (array) má v Go pevnou délku, která je součástí jeho typu, například `[3]int`; ukládá své prvky přímo a přiřazení nebo předání pole zkopíruje celou jeho hodnotu. Řez (slice), například `[]int`, je malý deskriptor nad podkladovým polem: koncepčně obsahuje ukazatel na prvky, délku a kapacitu. Délka řezu je počet viditelných prvků; jeho kapacita určuje, kolik prvků lze použít od začátku řezu, než se dosáhne konce podkladového pole. Řezy jsou flexibilní: opětovné vytvoření řezu (reslicing) změní deskriptor a funkce `append` může znovu použít stejné podkladové pole, pokud to kapacita dovoluje, nebo alokovat nové, pokud ne.

Zkusit odpovědět na tuto otázku s trenérem AI

8Jak se chová typ map v jazyce Go, pokud jde o typy klíčů, chybějící klíče, mapy s hodnotou nil a pořadí iterace?

Typy klíčů mapy v jazyce Go musí být porovnatelné (comparable); řezy (slices), mapy a funkce nelze použít přímo jako klíče. Vyhledání klíče, který není přítomen, vrací nulovou hodnotu daného typu prvku (zero value), proto se používá forma comma-ok (`v, ok := m[k]`) k rozlišení nepřítomnosti od přítomné nulové hodnoty. Z mapy s hodnotou `nil` lze číst a lze přes ni iterovat pomocí `range`, ale přiřazení do ní vyvolá chybu `panic`; před zápisem ji proto inicializujte. Pořadí iterace mapy není specifikováno a kód na něm nesmí záviset.

Zkusit odpovědět na tuto otázku s trenérem AI

9Popište, jak může opětovné vytvoření řezu (reslicing) a přiřazení způsobit, že více řezů bude sdílet stejné podkladové pole, a jaké chyby to může způsobit.

Hodnota řezu je hlavička ukazující do podkladového pole. Přiřazení řezu nebo jeho předání funkci zkopíruje pouze tuto hlavičku, nikoli samotné prvky. Opětovné vytvoření řezu (reslicing) vytvoří další hlavičku ukazující na rozsah stejného podkladového pole. Proto může více řezů odkazovat na stejné úložiště: změna prvku prostřednictvím jednoho řezu může být viditelná v jiném a použití funkce `append` na jeden řez může přepsat data viditelná pro jiný řez, pokud má stále volnou kapacitu. Mezi chyby patří neočekávané mutace, poškozené výsledky, zadržování velkých podkladových polí v paměti kvůli malým podřízeným řezům (subslices) a souběhy dat (data races), když jsou aliasy používány souběžně. Chcete-li se vyhnout nechtěnému sdílení, vytvořte defenzivní kopii pomocí funkce `copy` nebo `append([]T(nil), s...)`, případně omezte kapacitu pomocí výrazu pro úplný řez (full-slice expression) před voláním `append`.

Zkusit odpovědět na tuto otázku s trenérem AI

10Vysvětlete koncept růstu řezu (slice) během operace append a výkonnostní důsledky opakované realokace.

Když `append` přidává prvky do řezu (slice), zapisuje do existujícího podkladového pole, pokud má řez dostatečnou kapacitu. Pokud je kapacita nedostatečná, Go alokuje větší podkladové pole, zkopíruje stávající prvky, zapíše nové prvky a vrátí hlavičku řezu (slice header) ukazující na nové úložiště. Přesná strategie růstu závisí na implementaci, ale koncepčně roste kapacita natolik, aby byly opakované operace `append` z hlediska amortizované složitosti efektivní. Opakované realokace stále spotřebovávají čas procesoru na kopírování, vytvářejí alokace, zvyšují zátěž GC (Garbage Collector) a mohou rozbít sdílení se starými aliasy řezu. Pokud znáte očekávanou velikost, předem proveďte alokaci pomocí `make([]T, 0, n)` při budování pomocí `append` nebo `make([]T, n)` při plnění pomocí indexu, abyste omezili realokace.

Zkusit odpovědět na tuto otázku s trenérem AI

Sémantika jazyka

11Jak v Go funguje prázdný identifikátor (blank identifier) pro nevyužité hodnoty, importy a kontroly rozhraní v době překladu?

Prázdný identifikátor (blank identifier) `_` funguje jako zástupný symbol pouze pro zápis (write-only). Přiřazení do tohoto identifikátoru hodnotu zahodí a nevytvoří použitelnou proměnnou. Používá se k ignorování nepotřebných návratových hodnot nebo proměnných v cyklech, k importu balíčku pouze kvůli jeho vedlejším efektům pomocí `import _ "pkg"`, a k ověření implementace rozhraní (interface) v době překladu, například `var _ io.Reader = (*MyReader)(nil)`. Tzv. prázdný import (blank import) stále spouští inicializaci importovaného balíčku. Přiřazení sloužící ke kontrole rozhraní selže při kompilaci, pokud `method set` konkrétního typu dané rozhraní nesplňuje.

Zkusit odpovědět na tuto otázku s trenérem AI

Balíčky

12Jak funguje pořadí inicializace balíčků v Go, včetně funkcí `init` a importovaných závislostí?

Go inicializuje balíčky v pořadí jejich závislostí. Importované závislosti balíčku jsou inicializovány před samotným importujícím balíčkem. V rámci balíčku jsou proměnné na úrovni balíčku inicializovány před jakýmikoli funkcemi `init`, přičemž inicializace proměnných je seřazena podle závislostí a pořadí deklarace, jak to definuje jazyk. Následně se automaticky spustí funkce `init` balíčku; balíček může mít více funkcí `init` a nelze je volat přímo. Každý balíček je inicializován pouze jednou. Pro spustitelný program je nejprve inicializován graf importů, poté je inicializován balíček `main` a nakonec je zavolána funkce `main.main`.

Zkusit odpovědět na tuto otázku s trenérem AI

13Vysvětlete pravidla viditelnosti balíčků v jazyce Go, včetně exportovaných identifikátorů a konvence adresáře internal/.

V jazyce Go je viditelnost v rámci balíčku řízena pojmenováním identifikátorů, nikoliv přístupovými modifikátory. Identifikátor, jehož název začíná velkým písmenem ze sady kódování Unicode, je exportován a může být odkazován z jiných balíčků; ostatní identifikátory jsou neexportované a lze je použít pouze v rámci stejného balíčku. To platí pro funkce, typy, metody, proměnné, konstanty a pole struktur (struct fields). Balíčky využívají exportované identifikátory k definování svého veřejného API (Application Programming Interface) a uchovávají detaily implementace jako neexportované. Kromě toho platí, že balíček umístěný v adresáři internal/ smí být importován pouze kódem, jehož cesta importu se nachází v rodičovském stromu daného adresáře internal; to je vynucováno sadou nástrojů jazyka Go (Go toolchain).

Zkusit odpovědět na tuto otázku s trenérem AI

Řízení toku programu

14Jak funguje `defer` v Go, včetně pořadí provádění, doby vyhodnocení argumentů a interakce s návratovými hodnotami?

Příkaz `defer` naplánuje volání funkce tak, aby se spustilo při opouštění nadřazené funkce, ať už tato končí běžným návratem (return) nebo rozbalováním při chybě (panic unwinding). Více odložených volání se provádí v pořadí „poslední dovnitř, první ven“ (LIFO). Hodnota odložené funkce a její argumenty jsou vyhodnoceny okamžitě při provedení příkazu `defer`, ale samotné volání proběhne až později. V případě pojmenovaných návratových hodnot (named return values) příkaz `return` nejprve přiřadí návratové hodnoty a teprve poté se spustí odložené funkce, takže odložená uzávěrka (closure) může pozorovat nebo upravovat pojmenované proměnné výsledku ještě předtím, než je volající obdrží. Díky tomu je `defer` mimořádně užitečný pro úklidové operace, jako je zavírání souborů, odemykání mutexů a uvolňování prostředků.

Zkusit odpovědět na tuto otázku s trenérem AI

Zpracování chyb

15Vysvětlete model zpracování chyb v jazyce Go a konvenční způsoby, jak jsou chyby vytvářeny, vraceny a kontrolovány.

Jazyk Go zachází s chybami jako s běžnými hodnotami, nikoli jako s výjimkami. Vestavěné rozhraní `error` je splněno jakýmkoli typem, který obsahuje metodu `Error() string`. Funkce obvykle konvenčně vrací `error` jako svůj poslední výsledek, přičemž `nil` znamená úspěch a nenulová (non-nil) chyba znamená, že volající musí selhání zpracovat nebo jej předat dále. Jednoduché chyby se běžně vytvářejí pomocí `errors.New`, formátované chyby pomocí `fmt.Errorf` a volající obvykle provádí kontrolu pomocí `if err != nil { ... }`.

Zkusit odpovědět na tuto otázku s trenérem AI

16Jak by mělo být v backendových službách v Go ošetřeno zotavení z paniky (panic recovery), včetně toho, co se stane, když v `goroutine` dojde k panice, a kdy by se měl proces zotavit versus spadnout (crash)?

Panika (panic) rozvine (unwinds) aktuální `goroutine` a spustí její odložené funkce (deferred functions). Funkce `recover` funguje pouze tehdy, když je zavolána z odložené funkce ve stejné `goroutine`; jedna `goroutine` nemůže zachytit paniku jiné `goroutine`. Pokud panika není zachycena, proces spadne. V backendových službách by zotavení mělo být obvykle umístěno na hranicích izolace, jako jsou obslužné rutiny požadavků (request handlers), RPC (Remote Procedure Call) middleware nebo vstupní body pracovních `goroutine` (worker goroutines), aby jeden selhávající požadavek nebo úloha neshodil celou službu. Pokud však panika mohla poškodit sdílený stav nebo způsobit nedůvěryhodnost integrity procesu, je bezpečnější nechat proces spadnout a restartovat jej, než se zotavit a slepě pokračovat.

Zkusit odpovědět na tuto otázku s trenérem AI

Souběžnost

17Co jsou v Go nil kanály (channels) a jak mohou nechtěně rozbít kód nebo záměrně deaktivovat větve příkazu select?

`nil` kanál je proměnná typu kanál, jejíž hodnota je `nil`, často proto, že nebyla inicializována pomocí funkce `make` nebo byla na `nil` explicitně nastavena. Odesílání do `nil` kanálu nebo přijímání z něj blokuje navždy. V příkazu `select` není větev (`case`) obsahující `nil` kanál nikdy připravena, takže přiřazení kanálu na hodnotu `nil` může danou větev záměrně deaktivovat. Neúmyslné použití `nil` kanálu může způsobit zamrznutí goroutine nebo to, že logika příkazu `select` přestane zpracovávat očekávané události.

Zkusit odpovědět na tuto otázku s trenérem AI

18Jak se atomické operace v balíčku `sync/atomic` liší od synchronizace založené na mutexech a kdy je vhodné je použít?

Balíček `sync/atomic` poskytuje nedělitelné operace nad jednotlivými paměťovými místy, jako jsou load, store, add, swap a compare-and-swap, se zárukami synchronizace a uspořádání paměti (memory-ordering). Mutex chrání kritickou sekci, takže může střežit libovolný kód a invarianty zahrnující vícero čtení, zápisů nebo položek. Atomické operace jsou vhodné pro jednoduchý nezávislý stav, jako jsou čítače, příznaky, sekvenční čísla nebo pečlivě navržené neblokující (lock-free) datové struktury. Dejte přednost mutexu, pokud jsou operace složené, více hodnot musí zůstat konzistentních, nebo by bylo obtížné pochopit a dokázat správnost atomické verze.

Zkusit odpovědět na tuto otázku s trenérem AI

19Jak by mělo být navrženo vlastnictví kanálů a životnost gorutin, aby se předešlo únikům gorutin (goroutine leaks)?

Navrhněte gorutiny s explicitním vlastníkem, jasným signálem k ukončení a zaručenou cestou k opuštění. Strana producenta obecně vlastní zavírání kanálu, zejména výstupního kanálu; přijímající strana by neměla zavírat kanál, pokud mohou být odesílatelé stále aktivní. U každého blokujícího odesílání, přijímání, cyklu, časovače nebo externího volání by mělo být buď zaručeno jeho dokončení, nebo by mělo umožňovat odblokování při zrušení, a to typicky pomocí `context.Context` nebo kanálu `done`. Použijte `WaitGroup`, `errgroup` nebo podobnou koordinaci, aby se počkalo na dokončení workerů a kanály se zavřely až po ukončení odesílatelů.

Zkusit odpovědět na tuto otázku s trenérem AI

20Jaké jsou běžné příčiny úniků `goroutine` (goroutine leaks) ve službách v Go a jak je v produkci detekujete a opravíte?

Běžné úniky `goroutine` ve službách v Go pocházejí z `goroutine`, které jsou trvale zablokované při odesílání nebo přijímání přes kanál (channel), čekají na jiné blokující operace bez možnosti zrušení (cancellation), ze zaseknutých I/O operací bez časových limitů (deadlines), z cyklů na pozadí nebo časovačů (tickers), které se nikdy nezastaví, a z `goroutine` vázaných na požadavek, které tento požadavek přežijí. V produkci hledáte trvalý růst počtu `goroutine` a související příznaky, následně prozkoumáte výpisy `goroutine` nebo profily `goroutine` z nástroje `pprof`, abyste zjistili, kde jsou `goroutine` zaseknuté. Oprava úniku znamená upravit kód tak, aby tyto `goroutine` mohly skončit: přidat zrušení a časové limity, zastavit časovače (tickers), správně zavřít kanály, vyhnout se odtrženým `goroutine` požadavků a omezit souběžnost (concurrency) tam, kde je to nutné.

Zkusit odpovědět na tuto otázku s trenérem AI