Middle-Go-Vorbereitung

Interviewfragen für Middle-Go-Backend-Entwickler

15 ausgewählte Fragen für Middle-Go-Entwickler, die Kompromisse, Nebenläufigkeit und Service-Verhalten erklären müssen.

Middle-Go-KI-Interview startenKeine Kreditkarte erforderlich. 1 kostenlose Session verfügbar.
Technische Interviews auf Englisch übenEin Modus, in dem Nicht-Muttersprachler technische Interviews üben können.

Typsystem

1Erklären Sie, wie nil in Go für Pointer, slices, maps, channels, Funktionen und Interfaces unterschiedlich funktioniert.

In Go ist nil der Nullwert für Pointer, slices, maps, channels, Funktionen und Interfaces, aber die Operationen auf diesen nil-Werten unterscheiden sich je nach Typ. Ein nil-Pointer kann mit nil verglichen werden, aber sein Dereferenzieren führt zu einem panic. Ein nil-slice hat Länge und Kapazität 0 und kann mit range iteriert und mit append erweitert werden. Eine nil-map kann gelesen und mit range iteriert werden, aber eine Zuweisung in sie führt zu einem panic. Senden an einen nil-channel oder Empfangen von einem nil-channel blockiert für immer, und das Schließen eines nil-channel führt zu einem panic. Das Aufrufen einer nil-Funktion führt zu einem panic. Ein Interface ist nur dann nil, wenn es weder einen dynamischen Typ noch einen dynamischen Wert hat; ein Interface, das einen typed nil-Wert enthält, etwa einen nil-Pointer, ist selbst nicht nil.

Diese Frage mit einem KI-Coach beantworten

2Was sind vergleichbare Typen in Go, und wie wirken sich die Regeln zur Vergleichbarkeit auf map-Schlüssel, Gleichheit und Constraints in Generics aus?

Vergleichbare Typen in Go sind Typen, deren Werte mit `==` und `!=` verglichen werden können. Grundtypen, Pointer, channels, Interfaces und structs/Arrays, deren Felder oder Elemente vergleichbar sind, sind vergleichbar; slices, maps und Funktionen sind außer im Vergleich mit `nil` nicht vergleichbar. Map-Schlüssel müssen vergleichbar sein. Gleichheit folgt den Vergleichsregeln des Typs, und der Vergleich von Interfaces hängt von den dynamischen konkreten Werten ab; enthält ein verglichenes Interface einen nicht vergleichbaren dynamischen Wert, führt der Vergleich zu einem panic. In Generics erlaubt das vordefinierte Constraint `comparable`, Typparameter mit `==`/`!=` zu vergleichen und als map-Schlüssel zu verwenden.

Diese Frage mit einem KI-Coach beantworten

3Wie stellt Go Bytes, runes und UTF-8-kodierten Text dar, und warum kann len(s) von der Anzahl der für Nutzer sichtbaren Zeichen abweichen?

In Go ist `byte` ein Alias für `uint8` und repräsentiert ein rohes Byte, während `rune` ein Alias für `int32` ist und einen Unicode-Code-Point repräsentiert. Ein `string` ist eine schreibgeschützte Folge von Bytes, meist UTF-8-kodierter Text, kann aber beliebige Bytes enthalten. `len(s)` gibt die Anzahl der Bytes zurück, nicht die Anzahl der runes oder sichtbaren Zeichen. Das Indexieren eines string liefert ein Byte; Iteration mit range über einen string dekodiert UTF-8 und liefert Byte-Indizes plus runes. `len(s)` kann von der Anzahl sichtbarer Zeichen abweichen, weil UTF-8 für manche Code-Points mehrere Bytes verwendet und weil ein für Nutzer sichtbares Zeichen aus mehreren Code-Points bestehen kann, etwa durch kombinierende Zeichen oder Emoji-Sequenzen.

Diese Frage mit einem KI-Coach beantworten

4Worin unterscheiden sich Type Aliases von Defined Types, und wann würdest du jeweils welchen verwenden?

Ein Defined Type, z. B. `type UserID int64`, erzeugt einen neuen, eigenständigen Typ mit `int64` als Underlying Type. Er ist ohne Konvertierung nicht frei zu `int64` zuweisbar und kann eigene Methoden haben. Ein Type Alias, z. B. `type UserID = int64`, ist nur ein anderer Name für denselben Typ, sodass Typidentität und Zuweisbarkeit erhalten bleiben. Verwende Defined Types für Domain-Modellierung, Typsicherheit und Methoden; verwende Aliases vor allem für Refactoring, Migration oder Kompatibilität, ohne einen neuen Typ einzuführen.

Diese Frage mit einem KI-Coach beantworten

5Wie behandelt Go besondere Gleitkommawerte, und welche Gleichheitsfallen sind in Backend-Systemen relevant?

Go `float32` und `float64` folgen IEEE-754-artigem Verhalten, einschließlich besonderer Werte wie positiver/negativer Unendlichkeit und NaN. Für `float64` gibt es Helfer wie `math.Inf`, `math.IsInf`, `math.NaN` und `math.IsNaN`. NaN ist mit nichts gleich, auch nicht mit sich selbst, sodass `x == x` falsch ist, wenn `x` NaN ist. Exakte Gleichheit bei berechneten Floats ist ebenfalls riskant, weil Rundung und Präzision mathematisch gleiche Werte unterscheiden können; verwende domänengeeignete Toleranzen oder vermeide Floats für exakte Geschäftswerte wie Geld. Floats sind als Map-Keys erlaubt, aber NaN-Keys sind problematisch, weil Map-Lookup von Gleichheit abhängt und NaN nicht gleich ist, nicht einmal mit sich selbst.

Diese Frage mit einem KI-Coach beantworten

6Beschreibe struct embedding in Go und wie sich promovierte Felder und Methoden verhalten.

Struct embedding in Go bedeutet, ein Feld über seinen Typ ohne expliziten Feldnamen zu deklarieren, zum Beispiel `type User struct { Person }`. Der eingebettete Wert ist weiterhin ein echtes Feld, zugreifbar als `u.Person`, aber seine exportierten/zugänglichen Felder und Methoden können promotet werden, sodass Aufrufer Selektoren wie `u.Name` oder `u.Greet()` als Kurzform für den Zugriff über das eingebettete Feld schreiben können. Embedding ist Komposition, keine klassische Vererbung: Der äußere Typ ist nicht automatisch ein Subtyp des eingebetteten Typs. Wenn promovierte Selektoren in Konflikt geraten, rät Go nicht; mehrdeutige Namen müssen qualifiziert werden oder sind über den äußeren Wert nicht auswählbar.

Diese Frage mit einem KI-Coach beantworten

Datenstrukturen

7Beschreiben Sie, wie Reslicing und Zuweisung von slices dazu führen können, dass mehrere slices dasselbe zugrunde liegende Array gemeinsam nutzen, und welche Bugs dadurch entstehen können.

Ein slice-Wert ist ein Header, der auf ein zugrunde liegendes Array zeigt. Das Zuweisen eines slice oder seine Übergabe an eine Funktion kopiert nur diesen Header, nicht die Elemente. Reslicing erzeugt einen weiteren Header, der auf einen Bereich desselben backing array zeigt. Daher können mehrere slices dasselbe Storage aliasen: Eine Änderung eines Elements über ein slice kann über ein anderes sichtbar werden, und append auf ein slice kann Daten überschreiben, die über ein anderes sichtbar sind, wenn noch freie Kapazität vorhanden ist. Zu den Bugs gehören überraschende Mutationen, korrupte Ergebnisse, das Behalten großer backing arrays über kleine subslices sowie Data Races, wenn Aliase gleichzeitig verwendet werden. Um unbeabsichtigtes Sharing zu vermeiden, macht man mit copy oder append([]T(nil), s...) eine defensive Kopie oder begrenzt die Kapazität vor append mit einem full-slice expression.

Diese Frage mit einem KI-Coach beantworten

8Erklären Sie das Wachstum von slices bei append auf konzeptioneller Ebene und die Performance-Auswirkungen wiederholter Reallokation.

Wenn append Elemente zu einem slice hinzufügt, schreibt es in das vorhandene backing array, sofern das slice genügend Kapazität hat. Reicht die Kapazität nicht aus, allokiert Go ein größeres backing array, kopiert die vorhandenen Elemente, schreibt die neuen Elemente und gibt einen slice header zurück, der auf den neuen Speicher zeigt. Die genaue Wachstumsstrategie ist implementierungsabhängig, aber konzeptionell wächst die Kapazität so, dass wiederholtes append amortisiert effizient ist. Wiederholte Reallokationen kosten dennoch CPU für das Kopieren, erzeugen Allokationen, erhöhen den GC-Druck und können Sharing mit alten slice-Aliasen aufbrechen. Wenn Sie die erwartete Größe kennen, allokieren Sie mit make([]T, 0, n) vor, wenn per append aufgebaut wird, oder mit make([]T, n), wenn per Index gefüllt wird, um Reallokationen zu reduzieren.

Diese Frage mit einem KI-Coach beantworten

Pakete

9Was sind Registrierungs-Patterns zur `init`-Zeit in Go, und welche Risiken bringen Side Effects durch Blank Imports und globale Registries mit sich?

Ein Registrierungs-Pattern zur `init`-Zeit liegt vor, wenn ein Paket aus einer `init`-Funktion heraus eine Implementierung in einer gemeinsamen Registry registriert. Ein Blank Import wie `_ "example.com/driver"` wird häufig verwendet, um ein Paket nur wegen seiner Side Effects zu importieren, sodass seine `init`-Funktion ausgeführt wird, obwohl keine exportierten Namen referenziert werden. Das ist bei Erweiterungspunkten für Treiber, Codecs, Plugins, Metriken oder Serializer verbreitet. Die Risiken sind versteckte Abhängigkeiten und Side Effects beim Start, globaler veränderbarer Zustand, doppelte oder reihenfolgenabhängige Registrierung, schwierigere Testisolierung und weniger explizites Verdrahten von Abhängigkeiten. Das sollte bewusst eingesetzt, klar dokumentiert und oft durch explizite Registrierung, idempotente/concurrency-safe Registries oder injizierbare/zurücksetzbare Registries für Tests entschärft werden.

Diese Frage mit einem KI-Coach beantworten

Fehlerbehandlung

10Wie wirken `defer`, `panic` und benannte Rückgabewerte zusammen, wenn man Cleanup implementiert, das zurückgegebene Fehler möglicherweise verändert?

Deferred Functions werden ausgeführt, nachdem Rückgabewerte zugewiesen wurden, aber bevor die Funktion an ihren Aufrufer zurückkehrt. Deshalb kann eine deferred Closure benannte Rückgabewerte wie ein benanntes `err` lesen oder verändern. Das wird häufig genutzt, um Cleanup-Fehler von `Close`, `Commit` oder ähnlichen Operationen dem zurückgegebenen Fehler hinzuzufügen, wobei idealerweise der primäre Fehler erhalten bleibt, statt überschrieben zu werden. Während des Panic-Unwinding werden deferred Functions weiterhin ausgeführt; eine deferred Function kann recovern und einen benannten Rückgabewert setzen, aber das sollte auf absichtliche Panic-Grenzen beschränkt bleiben. Achten Sie darauf, einen benannten Rückgabewert wie `err` nicht zu shadowen, denn `defer` könnte sonst eine andere Variable als beabsichtigt beobachten oder verändern.

Diese Frage mit einem KI-Coach beantworten

11Wie funktionieren errors.Is, errors.As und `%w` in Go bei Fehlerketten mit Wrapping?

`fmt.Errorf` mit `%w` erzeugt einen neuen Fehler, der einen zugrunde liegenden Fehler wrappt und dabei Kontext hinzufügt. Wrapper legen zugrunde liegende Fehler über `Unwrap` offen und bilden so eine Kette oder einen Baum, den die Standardbibliothek untersuchen kann. `errors.Is(err, target)` prüft, ob `err` oder irgendetwas, das es wrappt, zu einem Ziel-Fehler passt. `errors.As(err, &target)` prüft, ob `err` oder irgendetwas, das es wrappt, einem Zieltyp zuweisbar ist, und speichert den passenden Wert im übergebenen Pointer.

Diese Frage mit einem KI-Coach beantworten

12Was sind Sentinel Errors, und welche Trade-offs gibt es im Vergleich zu eigenen typisierten Fehlern oder reichhaltigeren Domain-Fehlermodellen?

Ein Sentinel Error ist ein benannter Fehlerwert, oft eine paketweite Variable wie `var ErrNotFound = errors.New("not found")`, die einen bestimmten Zustand repräsentiert, den Aufrufer prüfen können – typischerweise mit `errors.Is`, wenn Wrapping möglich ist. Sentinels sind einfach und nützlich für grobe, stabile Kategorien, aber exportierte Sentinels werden Teil der API und können Aufrufer an bestimmte Werte koppeln. Eigene typisierte Fehler können strukturierte Felder tragen und mit `errors.As` gefunden werden. Reichhaltigere Domain-Fehlermodelle klassifizieren Fehler nach Art/Code und können sichere Meldungen oder Metadaten enthalten; das ist nützlich, wenn Aufrufer stabiles Verhalten über einen einzelnen festen Fehlerwert hinaus benötigen.

Diese Frage mit einem KI-Coach beantworten

13Wie sollte ein Go-Backend interne Fehler auf nützliche Client-Responses abbilden und gleichzeitig für Operatoren gut diagnostizierbare Signale liefern?

Ein Go-Backend sollte interne Fehler an einer Anwendungs- oder Transportgrenze in stabile, clientsichere Kategorien und Responses übersetzen. Diese Kategorien sollten auf passende HTTP-Statuscodes oder entsprechende Transport-Status abgebildet werden, mit sicheren Meldungen und maschinenlesbaren Codes statt roher interner Fehler. Operatoren sollten dennoch diagnostische Informationen über strukturierte Logs, Traces, Metriken, Korrelations-/Request-IDs und erhaltene zugrunde liegende Ursachen bekommen. Logging erfolgt üblicherweise am besten einmal an einer Grenze mit Request-Kontext, um sowohl fehlende Fehler als auch doppelte, laute Logs zu vermeiden.

Diese Frage mit einem KI-Coach beantworten

14Wie implementiert man eigene Fehlertypen in Go korrekt, und wie beeinflusst errors.Join die Untersuchung und die Fehlerbehandlung auf Cleanup-Pfaden?

Ein eigener Go-Fehlertyp erfüllt `error`, indem er `Error() string` implementiert. Er kann außerdem strukturierte Felder tragen und `Unwrap() error` implementieren, um eine zugrunde liegende Ursache offenzulegen. Pointer- versus Value-Receiver sind wichtig: Ein Pointer-Receiver bedeutet, dass nur `*T` `error` erfüllt, während ein Value-Receiver meist bedeutet, dass sowohl `T` als auch `*T` dies tun, was Kopien und den Typ beeinflusst, den Aufrufer mit `errors.As` verwenden sollten. `errors.Join` kombiniert mehrere Fehler zu einem Fehler; `errors.Is` und `errors.As` untersuchen die zusammengefügten Kinder. Das ist nützlich, wenn sowohl ein primärer Operationsfehler als auch ein Cleanup-/deferred-Fehler zurückgegeben werden sollen, ohne einen der beiden zu verlieren.

Diese Frage mit einem KI-Coach beantworten

Nebenläufigkeit

15Wie funktioniert `select` mit channels, einschließlich Auswahl unter bereiten Fällen, `default`-Fällen und abbrechbaren Operationen?

`select` wartet auf mehrere channel-Operationen und führt einen Fall aus, dessen Senden oder Empfangen fortgesetzt werden kann. Wenn kein channel-Fall bereit ist, blockiert es, außer es gibt einen `default`-Fall; `default` wird nur dann sofort ausgeführt, wenn keine channel-Operation fortgesetzt werden kann, was für nicht blockierende Sende-/Empfangsversuche nützlich ist. Wenn mehrere Fälle bereit sind, wählt Go einen davon pseudozufällig statt nach Quelltextreihenfolge. Abbrechbare channel-Operationen fügen häufig einen Fall hinzu, der von `ctx.Done()` empfängt, sodass die goroutine aufhören kann zu warten, wenn der context abgebrochen wird oder ein timeout eintritt.

Diese Frage mit einem KI-Coach beantworten