1Erklären Sie, wie die Nullwerte (zero values) in Go für eingebaute Typen und referenzähnliche Typen funktionieren und warum sie wichtig sind, wenn Variablen ohne explizite Initialisierung deklariert werden.
In Go wird eine Variable, die ohne expliziten Initialisierer deklariert wird, automatisch mit dem Nullwert ihres Typs initialisiert. Numerische Typen werden zu 0, bool wird zu false, string wird zu "", und Arrays oder structs werden Element für Element bzw. Feld für Feld auf den Nullwert gesetzt. Pointer- oder referenzähnliche Typen wie Pointer, slices, maps, channels, Funktionen und Interfaces haben nil als Nullwert. Das ist wichtig, weil Go-Variablen und ausgelassene struct-Felder in einem deterministischen Zustand starten, statt undefinierte Speicherinhalte zu enthalten, und viele APIs so entworfen sind, dass der Nullwert ein nützlicher Standard ist, auch wenn einige nil-Werte vor bestimmten Operationen dennoch initialisiert werden müssen.
2Wie behandelt Go Gleichheit bei structs, und was passiert, wenn ein struct nicht vergleichbare Felder enthält?
Struct-Werte in Go können nur dann mit `==` und `!=` verglichen werden, wenn jedes Feld im struct vergleichbar ist. Gleichheit vergleicht entsprechende Felder nach der jeweiligen Gleichheitsregel des Feldtyps. Enthält ein struct ein nicht vergleichbares Feld wie ein slice, eine map oder eine Funktion, ist der struct-Typ nicht vergleichbar, und der Vergleich zweier Werte dieses struct-Typs mit `==` ist ein Compile-Zeit-Fehler. Für solche structs sollte man benutzerdefinierte Vergleichslogik oder einen geeigneten Helfer für tiefe Gleichheit verwenden, besonders in Tests.
3Erklären Sie die Unveränderlichkeit von Strings in Go und die Beziehung zwischen string, []byte, bytes.Buffer und strings.Builder.
Ein Go-`string` ist eine unveränderliche Folge von Bytes, oft UTF-8-Text, aber nicht zwingend gültiges UTF-8. Einen string kann man nicht in place verändern; um Inhalte zu ändern, konvertiert man typischerweise zu `[]byte` für byteweise Änderungen oder zu `[]rune` für Änderungen auf Code-Point-Ebene und konvertiert dann zurück. Normale Konvertierungen zwischen `string` und `[]byte` kopieren Daten und können allozieren, daher können wiederholte Konvertierungen oder wiederholte Konkatenation in Schleifen teuer sein. `strings.Builder` ist für das effiziente Erzeugen von Strings optimiert, während `bytes.Buffer` ein veränderlicher Byte-Buffer ist, der für byteorientierte Daten und I/O nützlich ist und ebenfalls einen string liefern kann.
4Wie funktionieren Pointer in Go, und welche Operationen sind im Vergleich zu C bewusst nicht erlaubt?
Ein Go-Pointer ist ein typisierter Wert, der die Adresse eines anderen Werts hält, zum Beispiel zeigt *int auf einen int. Mit &x erhält man die Adresse eines Werts und mit *p dereferenziert man einen Pointer, um den referenzierten Wert zu lesen oder zu schreiben. Wenn man einen Pointer übergibt oder speichert, können mehrere Stellen denselben zugrunde liegenden Wert beobachten oder verändern, und ein Pointer kann nil sein. Anders als in C verbietet sicheres Go bewusst Pointer-Arithmetik und beliebige Manipulation von Rohadressen; Low-Level-Ausnahmen erfordern das Paket unsafe.
5Erkläre, wie Constants und iota in Go funktionieren, einschließlich typed versus untyped Constants.
Go-Constants sind Compile-Time-Werte: boolean, string oder numerisch. Eine typed Constant hat einen bestimmten Typ. Eine untyped Constant hat keinen festen konkreten Typ, bis sie in einem typisierten Kontext verwendet wird, und numerische untyped Constants werden bis dahin exakt/hochpräzise dargestellt; der Wert muss im gewählten Typ repräsentierbar sein. `iota` ist ein vordeklarierter Identifier in `const`-Deklarationen: Er startet in jedem const-Block bei 0 und erhöht sich für jede Constant-Spezifikation, was ihn für enum-artige Constants und Bit-Flags nützlich macht.
6Wie funktionieren explizite Konvertierungen in Go, und warum vermeidet die Sprache viele implizite Konvertierungen?
Go erfordert in der Regel explizite Konvertierungen mit `T(x)`, wenn sich der Typ eines Werts ändert, z. B. `int64(i)` oder `MyID(n)`. Dadurch bleiben Typänderungen sichtbar und überraschende implizite numerische, boolean- oder string-Konvertierungen werden vermieden. Konvertierungen sind nur nach Gos Konvertierungsregeln erlaubt und können den Wert ändern, z. B. durch Integer-Overflow/Truncation, Float-zu-Int-Truncation gegen null oder Präzisionsverlust. Untyped Constants sind flexibler: Sie können in einem typisierten Kontext verwendet werden, wenn der Constant-Wert in diesem Typ repräsentierbar ist.
7Beschreiben Sie den Unterschied zwischen Arrays und slices in Go, einschließlich des Verhaltens von Länge, Kapazität und zugrunde liegendem Speicher.
Ein Array in Go hat eine feste Länge, die Teil seines Typs ist, zum Beispiel [3]int; es speichert seine Elemente direkt, und das Zuweisen oder Übergeben eines Arrays kopiert den gesamten Array-Wert. Ein slice, zum Beispiel []int, ist ein kleiner Deskriptor über einem zugrunde liegenden Array: Konzeptionell enthält es einen Pointer auf die Elemente, eine Länge und eine Kapazität. Die Länge eines slice ist die Anzahl der sichtbaren Elemente; seine Kapazität ist, wie viele Elemente ab dem Anfang des slice genutzt werden können, bevor das Ende des backing array erreicht ist. Slices sind flexibel: Reslicing ändert den Deskriptor, und append kann dasselbe zugrunde liegende Array wiederverwenden, wenn die Kapazität ausreicht, oder ein neues allozieren, wenn nicht.
8Wie verhält sich der map-Typ in Go in Bezug auf Schlüsseltypen, fehlende Schlüssel, nil-maps und Iterationsreihenfolge?
Schlüsseltypen von Go-maps müssen vergleichbar sein; slices, maps und Funktionen können nicht direkt als Schlüssel verwendet werden. Das Nachschlagen eines nicht vorhandenen Schlüssels liefert den Nullwert des Elementtyps zurück, daher wird die comma-ok-Form (`v, ok := m[k]`) verwendet, um Abwesenheit von einem vorhandenen Nullwert zu unterscheiden. Eine nil-map kann gelesen und mit range iteriert werden, aber eine Zuweisung in sie führt zu einem panic; initialisieren Sie sie vor dem Schreiben. Die Iterationsreihenfolge von maps ist nicht spezifiziert, und Code darf sich nicht darauf verlassen.
9Wann sollte man in Go ein struct oder einen anderen Wert per Wert statt per Pointer übergeben?
Übergabe per Wert ist passend, wenn der Wert klein ist, im Aufruf nur gelesen wird oder unabhängige Kopiersemantik gewünscht ist. Übergabe per Pointer ist passend, wenn die Funktion den Wert des Aufrufers verändern muss, das Kopieren teuer wäre, der Typ nicht kopiert werden sollte oder nil/geteilte Identität Teil der API ist. Man sollte nicht annehmen, dass Pointer immer schneller sind: Sie können Aliasing, Heap-Escapes, GC-Arbeit und weniger cache-freundlichen Zugriff verursachen. Bevorzuge zuerst klare Semantik und miss performancekritische Entscheidungen mit Benchmarks.
10Wie verhalten sich Zuweisung und Kopieren bei structs, Arrays, maps und slices in Go?
Eine Zuweisung in Go kopiert den zugewiesenen Wert, aber die Wirkung hängt vom Typ ab. Eine struct-Zuweisung kopiert die struct-Felder, und eine Array-Zuweisung kopiert jedes Element. Eine slice-Zuweisung kopiert den Slice-Header — Pointer, Länge und Kapazität —, sodass slices normalerweise dasselbe Backing-Array teilen. Eine map-Zuweisung kopiert einen referenzähnlichen map-Deskriptor, sodass beide Variablen auf dieselben map-Daten verweisen. Für unabhängigen Besitz braucht man eine defensive Kopie, zum Beispiel mit copy oder slices.Clone für slices und durch Erzeugen einer neuen map plus Kopieren der Einträge für maps.
11Wie erfassen Closures in Go Variablen, und welche Bugs können rund um Schleifen und goroutines entstehen?
Eine Closure in Go ist ein Funktionswert, der auf Variablen aus seinem umgebenden lexikalischen Scope verweist. Sie erfasst die Variablen selbst, sodass Mutationen von der Closure beobachtet werden können, und erfasste Variablen können länger leben als die erzeugende Funktion, wenn die Closure entkommt. Bugs rund um Schleifen und goroutines entstehen, wenn Closures eine Variable gemeinsam nutzen oder beobachten, nachdem sie sich geändert hat; das führt oft dazu, dass goroutines oder verzögerte Callbacks einen unbeabsichtigten Wert sehen oder Data Races auf gemeinsam genutztem Zustand entstehen. Die übliche Lösung ist, den gewünschten Wert als Argument an die Closure zu übergeben oder pro Iteration eine neue lokale Kopie anzulegen, und konkurrierenden Zugriff bei Bedarf zu synchronisieren. Seit Go 1.22 sind Schleifenvariablen, die von `for`/`range` deklariert werden, pro Iteration getrennt, was viele klassische Capture-Bugs mit Schleifenvariablen beseitigt; wiederverwendete Variablen außerhalb der Schleife und anderer gemeinsam veränderbarer Zustand können aber weiterhin Probleme verursachen.
12Beschreibe die Semantik von range über Arrays, slices, maps, strings und channels.
`range` iteriert abhängig vom Operanden-Typ. Über einem Array oder slice liefert es einen Index und einen Elementwert; der Elementwert ist eine Kopie, daher verändert eine Zuweisung daran die Collection nicht. Das Iterieren mit range über einen Array-Wert kopiert das Array für die Iteration, während range über einen slice den Slice-Header verwendet und das zugrunde liegende Array indiziert. Über einer map liefert es Schlüssel und Wert in nicht festgelegter Reihenfolge, und der Wert ist eine Kopie. Über einem string liefert es den Byte-Index und den dekodierten Unicode-Codepunkt (`rune`), nicht einen Rune-Positionsindex. Über einem channel empfängt es Werte, bis der channel geschlossen und geleert ist; range über einen nil-channel blockiert für immer.
13Was ist der Unterschied zwischen Value Receivers und Pointer Receivers, und wie beeinflusst die Receiver-Wahl Mutierbarkeit, Kopieren und Interface-Erfüllung?
Eine Methode mit Value Receiver erhält eine Kopie des Receivers und eignet sich daher für schreibgeschütztes Verhalten und kleine, eher unveränderliche Typen; Änderungen an der Receiver-Kopie sind für den Aufrufer nicht sichtbar. Eine Methode mit Pointer Receiver erhält eine Kopie eines Pointers auf den Receiver, kann daher das Original verändern und das Kopieren großer oder nicht-kopierbarer Werte vermeiden. Die Receiver-Wahl beeinflusst Method Sets: Methoden mit Value Receiver gehören zum Method Set von T und *T, Methoden mit Pointer Receiver nur zum Method Set von *T. Daher wird ein Interface, das eine Pointer-Receiver-Methode verlangt, von *T erfüllt, nicht von T, auch wenn ein adressierbarer Wert vom Typ T die Methode mit normaler Methodenaufrufsyntax aufrufen kann.
14Beschreibe, wie Interfaces in Go funktionieren und was es bedeutet, dass ein Typ ein Interface implizit erfüllt.
Ein Go-Interface definiert eine Menge erforderlicher Methoden. Ein konkreter Typ erfüllt ein Interface implizit, wenn sein Method Set diese Methoden enthält; es gibt keine implements-Deklaration. Eine Interface-Variable kann einen konkreten dynamischen Wert halten, dessen Typ das Interface erfüllt, und Methodenaufrufe über das Interface dispatchen auf die Implementierung dieses dynamischen Werts. Das leere Interface, geschrieben als interface{} oder meist any, hat keine erforderlichen Methoden, daher erfüllt es jeder Typ. Ob T, *T oder beide ein Interface erfüllen, hängt von ihren Method Sets und der Receiver-Wahl ab.
15Wie funktionieren Type Assertions und Type Switches mit Interface-Werten?
Eine Type Assertion prüft den dynamischen Wert, der in einem Interface gespeichert ist: `v := x.(T)` gelingt, wenn der dynamische Typ des Interface-Werts `T` ist, oder wenn `T` ein Interface ist, das vom dynamischen Wert implementiert wird. Die Einzelergebnis-Form panickt bei Fehlschlag. Die Comma-ok-Form, `v, ok := x.(T)`, meldet Erfolg ohne Panic. Ein Type Switch, `switch v := x.(type)`, verzweigt anhand des dynamischen Typs des Interface-Werts, wobei `v` je nach gematchtem Case typisiert ist.