Voorbereiding op Go-interviews

Interviewvragen voor Go-backenddevelopers

Geselecteerde Go-interviewvragen voor backenddevelopers, gegroepeerd op onderwerp en afkomstig uit dezelfde catalogus die wordt gebruikt voor de oefeningen van EngineerSpeak.

Start een Go-backendinterview met AIGeen creditcard nodig. 1 gratis sessie beschikbaar.
Oefen technische interviews in het EngelsEen modus waarin anderstaligen kunnen oefenen met het afleggen van technische interviews.

Typesysteem

1Leg uit hoe de nulwaarden (zero values) van Go werken voor ingebouwde en referentieachtige typen, en waarom ze belangrijk zijn bij het declareren van variabelen zonder expliciete initialisatie.

In Go wordt een variabele die zonder expliciete initialisator is gedeclareerd, automatisch geïnitialiseerd op de nulwaarde van het bijbehorende type. Numerieke typen worden 0, `bool` wordt `false`, `string` wordt `""`, en arrays of `struct`-waarden worden element voor element of veld voor veld op nul gezet. Pointerachtige of referentieachtige typen zoals pointers, `slice`-waarden, `map`-typen, channels, functies en interfaces hebben `nil` als nulwaarde. Dit is belangrijk omdat Go-variabelen en weggelaten `struct`-velden in een deterministische toestand beginnen in plaats van willekeurige geheugenwaarden te bevatten. Bovendien zijn veel API's zo ontworpen dat de nulwaarde een bruikbare standaardwaarde is, hoewel sommige `nil`-waarden nog steeds initialisatie vereisen voor bepaalde bewerkingen.

Probeer deze vraag te beantwoorden met een AI-begeleider

2Hoe gaat Go om met gelijkheid voor structs en wat gebeurt er als een struct onvergelijkbare velden bevat?

`struct`-waarden in Go kunnen alleen met `==` en `!=` worden vergeleken als elk veld in de struct vergelijkbaar is. Gelijkheid vergelijkt de overeenkomstige velden met behulp van de eigen gelijkheidsregel van elk veld. Als een struct een onvergelijkbaar veld bevat, zoals een slice, map of functie, dan is het `struct`-type niet vergelijkbaar en is het vergelijken van twee waarden van dat `struct`-type met `==` een fout tijdens het compileren. Gebruik voor dergelijke structs aangepaste vergelijkingslogica of een geschikte hulpfunctie voor diepe gelijkheid, vooral in tests.

Probeer deze vraag te beantwoorden met een AI-begeleider

3Leg de onveranderlijkheid van strings in Go uit en de relatie tussen string, []byte, bytes.Buffer en strings.Builder.

Een `string` in Go is een onveranderlijke reeks bytes, vaak UTF-8-tekst, maar het is niet vereist dat het geldige UTF-8 is. Je kunt een string niet ter plekke wijzigen; om de inhoud te veranderen converteer je deze doorgaans naar `[]byte` voor bewerkingen op byteniveau of naar `[]rune` voor bewerkingen op codepoint-niveau, om het daarna weer terug te converteren. Normale conversies tussen `string` en `[]byte` kopiëren de gegevens en kunnen geheugen toewijzen (alloceren), waardoor herhaalde conversies of herhaaldelijk aaneenschakelen in lussen kostbaar kunnen zijn. `strings.Builder` is geoptimaliseerd voor het efficiënt opbouwen van strings, terwijl `bytes.Buffer` een veranderlijke bytebuffer is die nuttig is voor byte-georiënteerde data en I/O, en ook een string kan produceren.

Probeer deze vraag te beantwoorden met een AI-begeleider

4Leg uit hoe `nil` verschillend werkt voor pointers, `slice`-waarden, `map`-typen, channels, functies en interfaces in Go.

In Go is `nil` de nulwaarde voor pointers, `slice`-waarden, `map`-typen, channels, functies en interfaces, maar bewerkingen op deze `nil`-waarden verschillen per type. Een `nil`-pointer kan worden vergeleken met `nil`, maar het derefereren ervan veroorzaakt een panic. Een `nil`-slice heeft een lengte en capaciteit van 0, er kan overheen worden geïtereerd met `range` en aan worden toegevoegd met `append`. Een `nil`-map kan worden gelezen en met `range` worden doorgelopen, maar een toewijzing eraan veroorzaakt een panic. Het verzenden naar of ontvangen van een `nil`-channel blokkeert voor altijd, en het sluiten ervan veroorzaakt een panic. Het aanroepen van een `nil`-functie leidt tot een panic. Een interface is alleen `nil` wanneer deze geen dynamisch type en geen dynamische waarde heeft; een interface die een getypeerde `nil`-waarde bevat, zoals een `nil`-pointer, is zelf niet `nil`.

Probeer deze vraag te beantwoorden met een AI-begeleider

5Wat zijn vergelijkbare typen in Go, en hoe beïnvloeden de regels voor vergelijkbaarheid `map`-sleutels, gelijkheid en restricties voor generics?

Vergelijkbare typen in Go zijn typen waarvan de waarden vergeleken kunnen worden met `==` en `!=`. Basistypen, pointers, channels, interfaces en structs/arrays waarvan de velden of elementen vergelijkbaar zijn, zijn vergelijkbaar; slices, maps en functies zijn niet vergelijkbaar, behalve met `nil`. `map`-sleutels moeten vergelijkbaar zijn. Gelijkheid volgt de vergelijkingsregels van het type, en de vergelijking van interfaces hangt af van de dynamische concrete waarden; als een vergeleken interface een onvergelijkbare dynamische waarde bevat, veroorzaakt de vergelijking een panic. Bij generics staat de vooraf gedeclareerde `comparable`-restrictie toe dat typeparameters worden vergeleken met `==`/`!=` en worden gebruikt als `map`-sleutels.

Probeer deze vraag te beantwoorden met een AI-begeleider

6Hoe representeert Go bytes, runes en UTF-8-gecodeerde tekst, en waarom kan len(s) verschillen van het aantal voor de gebruiker zichtbare tekens?

In Go is `byte` een alias voor `uint8` en representeert het één ruwe byte, terwijl `rune` een alias is voor `int32` en een Unicode-codepoint vertegenwoordigt. Een `string` is een alleen-lezen reeks bytes, doorgaans UTF-8-gecodeerde tekst, maar is in staat om willekeurige bytes te bevatten. `len(s)` retourneert het aantal bytes, niet het aantal runes of voor de gebruiker zichtbare tekens. Het indexeren van een string retourneert een byte; itereren over een string met `range` decodeert UTF-8 en levert byte-indexen plus runes op. `len(s)` kan verschillen van het aantal zichtbare tekens omdat UTF-8 meerdere bytes per codepoint kan gebruiken, en omdat één voor de gebruiker zichtbaar teken kan bestaan uit meerdere codepoints, zoals combinerende diakritische tekens of emoji-reeksen.

Probeer deze vraag te beantwoorden met een AI-begeleider

Datastructuren

7Beschrijf het verschil tussen arrays en slices in Go, inclusief hoe de lengte, capaciteit en onderliggende opslag zich gedragen.

Een array in Go heeft een vaste lengte die onderdeel is van het type, zoals `[3]int`; de elementen worden direct opgeslagen, en bij het toewijzen of doorgeven van een array wordt de volledige arraywaarde gekopieerd. Een slice, zoals `[]int`, is een kleine descriptor bovenop een onderliggende array: conceptueel bevat het een pointer naar de elementen, een lengte en een capaciteit. De lengte van een slice is het aantal zichtbare elementen; de capaciteit geeft aan hoeveel elementen er vanaf het begin van de slice kunnen worden gebruikt voordat het einde van de onderliggende array wordt bereikt. Slices zijn flexibel: bij het aanpassen van de grenzen verandert de descriptor, en `append` kan dezelfde onderliggende array hergebruiken als de capaciteit dit toelaat, of een nieuwe toewijzen als dat niet het geval is.

Probeer deze vraag te beantwoorden met een AI-begeleider

8Hoe gedraagt het `map`-type in Go zich met betrekking tot sleuteltypen, ontbrekende sleutels, `nil`-maps en de volgorde van iteratie?

Sleuteltypen in een Go-`map` moeten vergelijkbaar zijn; `slice`-, `map`- en functietypen kunnen niet direct als sleutels worden gebruikt. Het opzoeken van een sleutel die niet aanwezig is, retourneert de nulwaarde van het elementtype, dus wordt de 'comma-ok'-notatie (`v, ok := m[k]`) gebruikt om afwezigheid te onderscheiden van een aanwezige nulwaarde. Uit een `nil`-map kan worden gelezen en er kan overheen worden geïtereerd, maar toewijzen eraan leidt tot een `panic`; initialiseer deze voor het schrijven. De iteratievolgorde van een `map` is niet gespecificeerd en code mag er niet van afhankelijk zijn.

Probeer deze vraag te beantwoorden met een AI-begeleider

9Beschrijf hoe het toewijzen en het aanpassen van de grenzen van een slice ertoe kunnen leiden dat meerdere slices dezelfde onderliggende array delen, en welke bugs dit kan veroorzaken.

De waarde van een slice is een header die verwijst naar een locatie in een onderliggende array. Bij het toewijzen van een slice of het doorgeven ervan aan een functie wordt alleen die header gekopieerd, niet de elementen zelf. Bij het afleiden van een nieuwe sub-slice ontstaat er een nieuwe header die naar (een deel van) dezelfde onderliggende array verwijst. Daardoor kunnen meerdere slices naar dezelfde opslaglocatie verwijzen: het wijzigen van een element via de ene slice kan zichtbaar zijn via de andere, en door `append` te gebruiken op de ene slice kunnen gegevens overschreven worden die zichtbaar zijn voor de andere, zolang er nog ongebruikte capaciteit is. Mogelijke bugs zijn onder meer onverwachte mutaties, beschadigde resultaten, het onnodig in het geheugen houden van grote onderliggende arrays door kleine sub-slices, en data races wanneer aliassen gelijktijdig worden gebruikt. Om onbedoeld delen te voorkomen, kun je een defensieve kopie maken met `copy` of `append([]T(nil), s...)`, of de capaciteit beperken via een 'full-slice expression' vóórdat je `append` aanroept.

Probeer deze vraag te beantwoorden met een AI-begeleider

10Leg de groei van een `slice` tijdens `append` op conceptueel niveau uit en benoem de prestatie-implicaties van herhaaldelijke herallocatie.

Wanneer `append` elementen toevoegt aan een `slice`, schrijft het naar de bestaande onderliggende array als de `slice` genoeg capaciteit heeft. Als de capaciteit onvoldoende is, wijst Go een grotere onderliggende array toe, kopieert de bestaande elementen, schrijft de nieuwe elementen en retourneert een `slice`-header die naar de nieuwe opslag wijst. Het exacte groeibeleid is afhankelijk van de implementatie, maar conceptueel groeit de capaciteit genoeg om herhaaldelijke `append`-aanroepen geamortiseerd efficiënt te maken. Herhaaldelijke herallocaties kosten nog steeds CPU-tijd voor het kopiëren, creëren geheugentoewijzingen, verhogen de druk op de GC (Garbage Collector) en kunnen het delen met oude `slice`-aliassen verbreken. Als je de verwachte grootte kent, wijs dan vooraf toe met `make([]T, 0, n)` bij het opbouwen via `append`, of met `make([]T, n)` bij het vullen op index, om herallocaties te verminderen.

Probeer deze vraag te beantwoorden met een AI-begeleider

Taalsemantiek

11Hoe werkt de 'blank identifier' in Go met betrekking tot ongebruikte waarden, imports en interface-controles tijdens de compilatie?

De *blank identifier* `_` is een tijdelijke aanduiding (placeholder) waar je alleen naar kunt schrijven. Bij een toewijzing hieraan wordt de waarde genegeerd en wordt er geen bruikbare variabele aangemaakt. Het wordt gebruikt om onnodige retourwaarden of lusvariabelen te negeren, een package uitsluitend voor diens neveneffecten te importeren met `import _ "pkg"`, en om tijdens de compilatie te controleren of een interface wordt geïmplementeerd via bijvoorbeeld `var _ io.Reader = (*MyReader)(nil)`. Bij een zogenoemde blank import wordt de initialisatie van het geïmporteerde package nog steeds uitgevoerd. Een toewijzing bedoeld als interface-controle resulteert in een compilatiefout als de methodenset van het concrete type niet aan de interface voldoet.

Probeer deze vraag te beantwoorden met een AI-begeleider

Packages

12Hoe werkt de initialisatievolgorde van packages in Go, inclusief `init`-functies en geïmporteerde afhankelijkheden?

Go initialiseert packages in volgorde van afhankelijkheid. De geïmporteerde afhankelijkheden van een package worden geïnitialiseerd vóór het package dat ze importeert. Binnen een package worden variabelen op packageniveau geïnitialiseerd vóór eventuele `init`-functies, waarbij de initialisatie van variabelen wordt geordend op basis van afhankelijkheid en declaratievolgorde zoals gedefinieerd door de taal. Vervolgens worden de `init`-functies van het package automatisch uitgevoerd; een package kan meerdere `init`-functies hebben en deze kunnen niet rechtstreeks worden aangeroepen. Elk package wordt eenmalig geïnitialiseerd. Voor een uitvoerbaar bestand wordt eerst de import-graaf geïnitialiseerd, vervolgens wordt het package `main` geïnitialiseerd en ten slotte wordt `main.main` aangeroepen.

Probeer deze vraag te beantwoorden met een AI-begeleider

13Leg de zichtbaarheidsregels voor pakketten in Go uit, inclusief geëxporteerde identifiers en de `internal/`-mapconventie.

In Go wordt de zichtbaarheid van pakketten bepaald door de naamgeving van identifiers, niet door toegangssleutelwoorden. Een identifier waarvan de naam begint met een Unicode-hoofdletter, wordt geëxporteerd en kan vanuit andere pakketten worden geraadpleegd; andere identifiers worden niet geëxporteerd en zijn alleen bruikbaar binnen hetzelfde pakket. Dit geldt voor functies, typen, methoden, variabelen, constanten en struct-velden. Pakketten gebruiken geëxporteerde identifiers om hun openbare API te definiëren en implementatiedetails niet te exporteren. Daarnaast mag een pakket dat zich in een `internal/`-map bevindt, alleen worden geïmporteerd door code waarvan het importpad zich binnen de bovenliggende boomstructuur van die `internal`-map bevindt; dit wordt afgedwongen door de Go-toolchain.

Probeer deze vraag te beantwoorden met een AI-begeleider

Control flow

14Hoe werkt defer in Go, inclusief de uitvoeringsvolgorde, het moment waarop argumenten worden geëvalueerd en de wisselwerking met retourwaarden?

`defer` plant een functie-aanroep in die uitgevoerd wordt wanneer de omliggende functie eindigt, ongeacht of dit gebeurt via een normale return of tijdens het afwikkelen van een panic. Meerdere uitgestelde aanroepen worden in last-in, first-out volgorde uitgevoerd. De uitgestelde functiewaarde en bijbehorende argumenten worden onmiddellijk geëvalueerd wanneer het `defer`-statement wordt uitgevoerd, maar de aanroep zelf vindt later plaats. Bij benoemde retourwaarden wijst een return-statement de retourwaarden eerst toe, waarna de uitgestelde functies draaien. Dit stelt een uitgestelde closure in staat om benoemde resultaatvariabelen te observeren of te wijzigen voordat de aanroeper deze ontvangt. Dit maakt `defer` zeer geschikt voor opruimtaken zoals het sluiten van bestanden, het ontgrendelen van mutexes en het vrijgeven van resources.

Probeer deze vraag te beantwoorden met een AI-begeleider

Foutafhandeling

15Leg het foutafhandelingsmodel van Go uit en de conventionele manieren waarop fouten worden aangemaakt, geretourneerd en gecontroleerd.

Go behandelt fouten als gewone waarden, niet als uitzonderingen. Aan de ingebouwde `error`-interface wordt voldaan door elk type met een `Error() string`-methode. Functies retourneren conventioneel een `error` als het laatste resultaat, waarbij `nil` succes betekent en een niet-nil fout betekent dat de aanroeper de fout moet afhandelen of doorgeven. Eenvoudige fouten worden doorgaans aangemaakt met `errors.New`, geformatteerde fouten met `fmt.Errorf`, en aanroepers controleren meestal via `if err != nil { ... }`.

Probeer deze vraag te beantwoorden met een AI-begeleider

16Hoe moet het herstel na een panic worden afgehandeld in Go backend-services, inclusief wat er gebeurt wanneer een goroutine in paniek raakt en wanneer een proces moet herstellen in plaats van crashen?

Een `panic` rolt de stack van de huidige goroutine af, waarbij de ingestelde `defer`-functies worden uitgevoerd. De functie `recover` werkt alleen wanneer deze wordt aangeroepen vanuit een `defer`-functie in diezelfde goroutine; de ene goroutine kan de `panic` van een andere goroutine niet herstellen. Als een `panic` niet wordt hersteld, crasht het proces. In backend-services moet herstel meestal worden geplaatst bij isolatiegrenzen zoals handlers voor aanvragen, RPC-middleware of startpunten van worker-goroutines, zodat één falende aanvraag of taak niet de hele service platlegt. Echter, als een `panic` mogelijk gedeelde status heeft beschadigd of de integriteit van het proces onbetrouwbaar heeft gemaakt, is het veiliger om het proces te laten crashen en herstarten in plaats van te herstellen en blindelings door te gaan.

Probeer deze vraag te beantwoorden met een AI-begeleider

Concurrency

17Wat zijn `nil`-channels in Go, en hoe kunnen ze per ongeluk code breken of opzettelijk `select`-cases uitschakelen?

Een `nil`-channel is een channel-variabele waarvan de waarde `nil` is, vaak omdat deze niet is geïnitialiseerd met `make` of expliciet op `nil` is ingesteld. Het verzenden naar of ontvangen van een `nil`-channel blokkeert voor altijd. In een `select` is een `case` met een `nil`-channel nooit gereed, dus het toewijzen van een channel-variabele aan `nil` kan die `case` opzettelijk uitschakelen. Het per ongeluk gebruiken van een `nil`-channel kan goroutines laten hangen of ervoor zorgen dat `select`-logica stopt met het afhandelen van verwachte gebeurtenissen.

Probeer deze vraag te beantwoorden met een AI-begeleider

18Waarin verschillen atomaire bewerkingen in sync/atomic van synchronisatie op basis van een mutex, en wanneer zijn ze geschikt?

sync/atomic biedt ondeelbare bewerkingen op individuele geheugenlocaties, zoals load, store, add, swap en compare-and-swap, met garanties voor synchronisatie en geheugenordening. Een mutex beschermt een kritieke sectie en kan willekeurige code en invarianten bewaken waarbij meerdere leesbewerkingen, schrijfbewerkingen of velden betrokken zijn. Atomaire bewerkingen zijn geschikt voor een eenvoudige, onafhankelijke status zoals tellers, vlaggen, volgnummers of zorgvuldig ontworpen lock-free datastructuren. Geef de voorkeur aan een mutex wanneer bewerkingen samengesteld zijn, meerdere waarden consistent moeten blijven, of wanneer de atomaire versie moeilijk te beredeneren of wiskundig te bewijzen is.

Probeer deze vraag te beantwoorden met een AI-begeleider

19Hoe moeten het eigenaarschap van channels en de levenscyclus van goroutines worden ontworpen om lekkende goroutines te voorkomen?

Ontwerp goroutines met een expliciete eigenaar, een duidelijk afsluitsignaal en een gegarandeerd eindpad. De producer-kant is over het algemeen verantwoordelijk voor het sluiten van een channel, met name een output-channel; ontvangers mogen een channel niet sluiten terwijl er mogelijk nog verzenders actief zijn. Elke blokkerende bewerking voor verzenden of ontvangen, elke lus, timer of externe aanroep moet gegarandeerd kunnen voltooien, of in staat zijn te deblokkeren bij annulering, meestal via `context.Context` of een `done`-channel. Gebruik `WaitGroup`, `errgroup` of een vergelijkbare vorm van coördinatie, zodat er op workers wordt gewacht en channels pas worden gesloten nadat de verzenders zijn gestopt.

Probeer deze vraag te beantwoorden met een AI-begeleider

20Wat zijn veelvoorkomende oorzaken van goroutine-lekken in Go-services, en hoe detecteer en verhelp je deze in productie?

Veelvoorkomende oorzaken van goroutine-lekken in Go-services zijn goroutines die permanent geblokkeerd blijven bij het verzenden of ontvangen via een kanaal, wachten op andere blokkerende bewerkingen zonder annulering, vastgelopen I/O zonder deadlines, achtergrondlussen of tickers die nooit stoppen, en request-gebonden goroutines die langer leven dan het request. In productie zoek je naar een aanhoudende toename in het aantal goroutines en gerelateerde symptomen, waarna je goroutine-dumps of pprof-goroutineprofielen inspecteert om te zien waar goroutines vastlopen. Het verhelpen van het lek vereist dat je de code aanpast zodat die goroutines kunnen afsluiten: voeg annulering en deadlines toe, stop tickers, sluit kanalen correct, vermijd losgekoppelde request-goroutines en begrens de gelijktijdigheid (concurrency) waar nodig.

Probeer deze vraag te beantwoorden met een AI-begeleider