1Leg 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`.
2Wat 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.
3Hoe 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.
4Wat is in Go het verschil tussen type-aliassen en gedefinieerde typen, en wanneer gebruik je welke?
Een gedefinieerd type, zoals `type UserID int64`, creëert een nieuw, afzonderlijk type met `int64` als onderliggend type. Het kan niet vrij worden toegewezen aan `int64` zonder expliciete conversie en kan zijn eigen methoden hebben. Een type-alias, zoals `type UserID = int64`, is simpelweg een alternatieve naam voor exact hetzelfde type, waardoor de identiteit en toewijsbaarheid behouden blijven. Gebruik gedefinieerde typen voor domeinmodellering, typeveiligheid en het toevoegen van methoden; gebruik aliassen voornamelijk voor refactoring, migraties of compatibiliteit, zonder dat er een compleet nieuw type hoeft te worden geïntroduceerd.
5Hoe gaat Go om met speciale floating-pointwaarden en welke valkuilen rondom gelijkheid zijn belangrijk in backendsystemen?
Go's `float32` en `float64` gebruiken gedrag in de stijl van IEEE-754, inclusief speciale waarden zoals positieve/negatieve oneindigheid en NaN. Voor `float64` zijn er hulpmiddelen beschikbaar zoals `math.Inf`, `math.IsInf`, `math.NaN` en `math.IsNaN`. NaN is aan niets gelijk, ook niet aan zichzelf, dus `x == x` is onwaar wanneer `x` NaN is. Exacte gelijkheid bij berekende floating-pointwaarden is eveneens riskant omdat afronding en precisie ertoe kunnen leiden dat wiskundig gelijke waarden toch verschillen; gebruik toleranties die passen bij het domein of vermijd floating-points voor exacte bedrijfswaarden zoals geld. Floating-pointwaarden zijn toegestaan als sleutels in een `map`, maar NaN-sleutels zijn problematisch omdat het opzoeken in een `map` afhankelijk is van gelijkheid en NaN niet als gelijk wordt beoordeeld, zelfs niet aan zichzelf.
6Beschrijf 'struct embedding' in Go en hoe gepromoveerde velden en methoden zich gedragen.
Struct-embedding in Go betekent het declareren van een veld met zijn type, maar zonder een expliciete veldnaam, bijvoorbeeld `type User struct { Person }`. De ingebedde waarde blijft een echt veld, toegankelijk als `u.Person`, maar de geëxporteerde of toegankelijke velden en methoden ervan kunnen worden gepromoveerd. Hierdoor kunnen aanroepers selectors zoals `u.Name` of `u.Greet()` schrijven als een kortere notatie, in plaats van de aanroep via het ingebedde veld te laten lopen. Inbedding is compositie, geen klassieke overerving: het buitenste type is niet automatisch een subtype van het ingebedde type. Als gepromoveerde selectors conflicteren, raadt Go niet; dubbelzinnige namen moeten worden gekwalificeerd, anders zijn ze niet te selecteren via de buitenste waarde.
7Beschrijf 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.
8Leg 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.
9Wat zijn init-time registratiepatronen in Go, en welke risico's brengen de bijwerkingen van blank imports en globale registers met zich mee?
Een init-time registratiepatroon is wanneer een package vanuit een `init`-functie een implementatie registreert in een gedeeld register. Een blank import zoals `_ "example.com/driver"` wordt vaak gebruikt om een package alleen te importeren vanwege zijn bijwerkingen, waardoor de `init`-functie ervan wordt uitgevoerd, zelfs als er niet naar geëxporteerde namen wordt verwezen. Dit is gebruikelijk voor uitbreidingspunten van drivers, codecs, plug-ins, metrics of serializers. De risico's zijn verborgen afhankelijkheden en bijwerkingen tijdens het opstarten, globale veranderlijke status, dubbele of volgordegevoelige registraties, lastigere testisolatie en een minder expliciete koppeling van afhankelijkheden. Het moet bewust worden gebruikt, duidelijk worden gedocumenteerd, en vaak worden verzacht door middel van expliciete registratie, idempotente/concurrency-veilige registers of injecteerbare/herstelbare registers voor tests.
10Hoe beïnvloeden `defer`, `panic` en benoemde retourwaarden elkaar bij het implementeren van opschoning die mogelijk geretourneerde fouten wijzigt?
Uitgestelde (`defer`) functies worden uitgevoerd nadat retourwaarden zijn toegewezen, maar voordat de functie terugkeert naar de aanroeper. Daarom kan een uitgestelde closure benoemde retourwaarden, zoals een benoemde `err`, lezen of wijzigen. Dit wordt vaak gebruikt om fouten bij het opschonen van `Close`, `Commit` of vergelijkbare bewerkingen toe te voegen aan de geretourneerde fout, waarbij in het ideale geval de oorspronkelijke fout behouden blijft en niet wordt overschreven. Tijdens het afwikkelen (unwinding) van een `panic` worden uitgestelde functies nog steeds uitgevoerd; een uitgestelde functie kan herstellen (recover) en een benoemde retourwaarde instellen, maar dit moet beperkt blijven tot opzettelijke `panic`-grenzen. Pas op dat je een benoemde retourvariabele zoals `err` niet verbergt (shadowing), omdat de `defer` dan een andere variabele kan waarnemen of wijzigen dan bedoeld is.
11Hoe werken errors.Is, errors.As en %w in ketens van verpakte fouten in Go?
`fmt.Errorf` met `%w` maakt een nieuwe fout aan die een onderliggende fout verpakt en tegelijkertijd context toevoegt. Wrappers maken onderliggende fouten toegankelijk via `Unwrap`, waardoor een keten of boomstructuur ontstaat die de standaardbibliotheek kan inspecteren. `errors.Is(err, target)` controleert of `err` of een door deze fout verpakte fout overeenkomt met een doelfout. `errors.As(err, &target)` controleert of `err` of een verpakte fout kan worden toegewezen aan het doeltype en slaat de gevonden waarde op in de opgegeven pointer.
12Wat zijn sentinel-errors, en wat zijn de voor- en nadelen ten opzichte van aangepaste getypeerde errors of rijkere domein-foutmodellen?
Een sentinel-error is een benoemde foutwaarde, vaak een variabele op pakketniveau zoals var ErrNotFound = errors.New("not found"), die wordt gebruikt om een specifieke conditie te vertegenwoordigen waarop aanroepende code kan testen, meestal met errors.Is wanneer wrapping mogelijk is. Sentinels zijn eenvoudig en nuttig voor brede, stabiele categorieën, maar geëxporteerde sentinels worden onderdeel van de API en kunnen aanroepende code koppelen aan specifieke waarden. Aangepaste getypeerde errors kunnen gestructureerde velden bevatten en worden gevonden met errors.As. Rijkere domein-foutmodellen classificeren fouten op soort of code en kunnen veilige berichten of metadata bevatten, wat handig is wanneer aanroepende code behoefte heeft aan stabiel gedrag dat verder gaat dan één vaste foutwaarde.
13Hoe moet een Go-backend interne fouten omzetten naar bruikbare responses voor de client, en tegelijkertijd beheerders diagnosticeerbare signalen bieden?
Een Go-backend moet interne fouten op de grens van de applicatie of transportlaag vertalen naar stabiele, veilige categorieën en responses voor de client. Deze categorieën moeten worden gekoppeld aan de juiste HTTP-statuscodes of vergelijkbare transportstatussen, met veilige berichten en machinaal leesbare codes in plaats van ruwe interne fouten. Systeembeheerders moeten nog steeds diagnostische informatie krijgen via gestructureerde logs, traces, metrics, correlatie- of request-ID's en het behoud van de onderliggende oorzaken. Loggen kan meestal het beste één keer gebeuren op een grens waar de context van de aanvraag beschikbaar is, om te voorkomen dat fouten onopgemerkt blijven of dat er te veel dubbele logs ontstaan.
14Hoe implementeer je aangepaste fouttypen correct in Go, en hoe beïnvloedt `errors.Join` inspectie en foutafhandeling bij opruimacties?
Een aangepast Go-fouttype voldoet aan `error` door `Error() string` te implementeren. Het kan ook gestructureerde velden bevatten en `Unwrap() error` implementeren om een onderliggende oorzaak bloot te leggen. De keuze tussen een pointer-receiver en een waarde-receiver is belangrijk: een pointer-receiver betekent dat alleen `*T` aan `error` voldoet, terwijl een waarde-receiver meestal betekent dat zowel `T` als `*T` hieraan voldoen. Dit heeft invloed op het kopiëren en op het type dat aanroepers in combinatie met `errors.As` moeten gebruiken. `errors.Join` combineert meerdere fouten tot één fout; `errors.Is` en `errors.As` inspecteren de samengevoegde onderliggende fouten. Dit is nuttig wanneer zowel de fout van een primaire bewerking als een fout bij het opruimen (of uitgestelde acties) moeten worden geretourneerd, zonder dat een van beide verloren gaat.
15Hoe werkt `select` met channels, inclusief de keuze voor een gereed zijnde `case`, de `default`-case en annuleerbare bewerkingen?
`select` wacht op meerdere channel-bewerkingen en voert één `case` uit waarvan de verzend- of ontvangstactie kan doorgaan. Als er geen channel-case gereed is, blokkeert het, tenzij er een `default`-case is; de `default`-case wordt alleen direct uitgevoerd wanneer geen enkele channel-bewerking kan doorgaan. Dit is nuttig voor niet-blokkerende pogingen om te verzenden of ontvangen. Als er meerdere cases gereed zijn, kiest Go er pseudo-willekeurig één uit in plaats van op basis van de volgorde in de broncode. Bij annuleerbare channel-bewerkingen wordt vaak een `case` toegevoegd die ontvangt van `ctx.Done()`, zodat de goroutine kan stoppen met wachten wanneer de context wordt geannuleerd of een time-out bereikt.