1 Grundlagen von Struct

In der Go-Sprache ist ein Struct ein zusammengesetzter Datentyp, der verwendet wird, um verschiedene oder identische Datentypen in eine einzige Einheit zu aggregieren. Structs nehmen in Go eine bedeutende Position ein, da sie ein grundlegender Aspekt der objektorientierten Programmierung darstellen, wenn auch mit leichten Unterschieden zu traditionellen objektorientierten Programmiersprachen.

Die Notwendigkeit von Structs ergibt sich aus den folgenden Aspekten:

  • Organisation von Variablen mit starker Relevanz, um die Code-Wartbarkeit zu verbessern.
  • Bereitstellung eines Mittels zur Simulation von "Klassen", um die Funktionen der Kapselung und Aggregation zu erleichtern.
  • Beim Umgang mit Datenstrukturen wie JSON, Datenbankeinträgen usw. bieten Structs ein praktisches Zuordnungswerkzeug.

Durch die Organisation von Daten mit Structs wird eine klarere Darstellung von realen Objektmodellen wie Benutzern, Bestellungen usw. ermöglicht.

2 Definition eines Struct

Die Syntax für die Definition eines Structs ist wie folgt:

type StructName struct {
    Feld1 Feldtyp1
    Feld2 Feldtyp2
    // ... andere Member-Variablen
}
  • Der type-Schlüsselwort führt die Definition des Structs ein.
  • StructName ist der Name des Struct-Typs, der gemäß den Benennungskonventionen von Go in der Regel großgeschrieben ist, um seine Exportfähigkeit anzuzeigen.
  • Das Schlüsselwort struct signalisiert, dass es sich um einen Struct-Typ handelt.
  • Innerhalb der geschweiften Klammern {} werden die Member-Variablen (Felder) des Structs definiert, wobei jedem Typ folgt.

Der Typ der Struct-Member kann jeder Typ sein, einschließlich Grundtypen (wie int, string, usw.) und Komplextypen (wie Arrays, Slices, ein anderer Struct usw.).

Beispiel für die Definition eines Structs, das eine Person darstellt:

type Person struct {
    Name   string
    Alter    int
    E-Mails []string // kann komplexe Typen wie Slices enthalten
}

Im obigen Code hat der Person-Struct drei Member-Variablen: Name vom Typ string, Alter vom Typ integer und E-Mails vom Typ String-Slice, was darauf hinweist, dass eine Person mehrere E-Mail-Adressen haben kann.

3 Erstellen und Initialisieren eines Structs

3.1 Erstellen einer Struct-Instanz

Es gibt zwei Möglichkeiten, eine Struct-Instanz zu erstellen: direkte Deklaration oder Verwendung des new-Schlüsselworts.

Direkte Deklaration:

var p Person

Der obige Code erstellt eine Instanz p vom Typ Person, wobei jede Member-Variable des Structs ihren entsprechenden Nullwert hat.

Verwendung des new-Schlüsselworts:

p := new(Person)

Das Erstellen eines Structs mit dem new-Schlüsselwort führt zu einem Zeiger auf den Struct. Die Variable p ist zu diesem Zeitpunkt vom Typ *Person und zeigt auf eine neu allokierte Variable vom Typ Person, bei der die Member-Variablen auf Nullwerte initialisiert wurden.

3.2 Initialisieren von Struct-Instanzen

Struct-Instanzen können bei ihrer Erstellung in einem einzigen Schritt initialisiert werden, und zwar mit oder ohne Feldnamen.

Initialisierung mit Feldnamen:

p := Person{
    Name:   "Alice",
    Alter:    30,
    E-Mails: []string{"[email protected]", "[email protected]"},
}

Bei der Initialisierung im Formular mit Feldzuweisung muss die Reihenfolge der Initialisierung nicht dieselbe sein wie die Reihenfolge der Deklaration des Structs, und alle nicht initialisierten Felder behalten ihre Nullwerte bei.

Initialisierung ohne Feldnamen:

p := Person{"Bob", 25, []string{"[email protected]"}}

Bei der Initialisierung ohne Feldnamen stellen Sie sicher, dass die Anfangswerte jeder Member-Variable in derselben Reihenfolge wie bei der Definition des Structs stehen und keine Felder ausgelassen werden können.

Zusätzlich können Structs mit spezifischen Feldern initialisiert werden, wobei alle nicht spezifizierten Felder Nullwerte übernehmen:

p := Person{Name: "Charlie"}

In diesem Beispiel wird nur das Name-Feld initialisiert, während Alter und E-Mails jeweils ihre entsprechenden Nullwerte übernehmen.

4 Zugriff auf Struct-Mitglieder

Der Zugriff auf die Member-Variablen eines Structs in Go ist sehr einfach und wird durch den Punkt (.) Operator erreicht. Wenn Sie eine Struct-Variable haben, können Sie ihre Member-Werte auf diese Weise lesen oder ändern.

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func main() {
    // Create a variable of type Person
    p := Person{"Alice", 30}

    // Access struct members
    fmt.Println("Name:", p.Name)
    fmt.Println("Age:", p.Age)

    // Modify member values
    p.Name = "Bob"
    p.Age = 25

    // Access the modified member values again
    fmt.Println("\nAktualisierter Name:", p.Name)
    fmt.Println("Aktualisiertes Alter:", p.Age)
}

In diesem Beispiel definieren wir zunächst eine Person Struktur mit zwei Elementvariablen, Name und Age. Wir erstellen dann eine Instanz dieser Struktur und zeigen, wie man diese Elemente liest und modifiziert.

5 Strukturzusammensetzung und Einbettung

Strukturen können nicht nur unabhängig existieren, sondern auch zusammengesetzt und ineinander verschachtelt werden, um komplexere Datenstrukturen zu erstellen.

5.1 Anonyme Strukturen

Eine anonyme Struktur deklariert nicht explizit einen neuen Typ, sondern verwendet stattdessen direkt die Strukturdefinition. Dies ist nützlich, wenn man eine Struktur einmal erstellen und einfach verwenden muss, um die Erstellung unnötiger Typen zu vermeiden.

Beispiel:

package main

import "fmt"

func main() {
    // Definiere und initialisiere eine anonyme Struktur
    person := struct {
        Name string
        Age  int
    }{
        Name: "Eve",
        Age:  40,
    }

    // Zugriff auf die Elemente der anonymen Struktur
    fmt.Println("Name:", person.Name)
    fmt.Println("Alter:", person.Age)
}

In diesem Beispiel definieren wir direkt eine Struktur anstatt einen neuen Typ zu erstellen. Dieses Beispiel zeigt, wie man eine anonyme Struktur initialisiert und auf ihre Elemente zugreift.

5.2 Struktureinbettung

Struktureinbettung beinhaltet das Verschachteln einer Struktur als Element einer anderen Struktur. Dadurch können wir komplexere Datenmodelle erstellen.

Beispiel:

package main

import "fmt"

// Definiere die Address-Struktur
type Address struct {
    City    string
    Country string
}

// Bette die Address-Struktur in die Person-Struktur ein
type Person struct {
    Name    string
    Age     int
    Address Address
}

func main() {
    // Initialisiere eine Person-Instanz
    p := Person{
        Name: "Charlie",
        Age:  28,
        Address: Address{
            City:    "New York",
            Country: "USA",
        },
    }

    // Zugriff auf die Elemente der eingebetteten Struktur
    fmt.Println("Name:", p.Name)
    fmt.Println("Alter:", p.Age)
    // Zugriff auf die Elemente der Address-Struktur
    fmt.Println("Stadt:", p.Address.City)
    fmt.Println("Land:", p.Address.Country)
}

In diesem Beispiel definieren wir eine Address-Struktur und betten sie als Element in die Person-Struktur ein. Beim Erstellen einer Instanz der Person erstellen wir gleichzeitig auch eine Instanz von Address. Wir können auf die Elemente der eingebetteten Struktur mittels Punktnotation zugreifen.

6 Strukturmethoden

Die Merkmale der objektorientierten Programmierung (OOP) können durch Strukturmethoden implementiert werden.

6.1 Grundkonzepte von Methoden

In der Go-Sprache, obwohl es kein traditionelles Konzept von Klassen und Objekten gibt, können ähnliche OOP-Funktionen durch das Binden von Methoden an Strukturen erreicht werden. Eine Strukturmethode ist eine spezielle Funktion, die mit einem bestimmten Strukturtyp (oder einem Zeiger auf eine Struktur) verbunden ist und es diesem Typ ermöglicht, seine eigenen Methoden zu haben.

// Definiere eine einfache Struktur
type Rechteck struct {
    Länge, Breite float64
}

// Definiere eine Methode für die Rechteck-Struktur zur Berechnung der Fläche des Rechtecks
func (r Rechteck) Fläche() float64 {
    return r.Länge * r.Breite
}

Im obigen Code ist die Methode Fläche mit der Struktur Rechteck verbunden. In der Methodendefinition ist (r Rechteck) der Empfänger, der angibt, dass diese Methode mit dem Typ Rechteck verbunden ist. Der Empfänger erscheint vor dem Methodennamen.

### 6.2 Wert- und Zeigerempfänger

Methoden können je nach Art des Empfängers in Wert- oder Zeigerempfänger eingeteilt werden. Wertempfänger verwenden eine Kopie des Structs zum Aufrufen der Methode, während Zeigerempfänger einen Zeiger auf das Struct verwenden und das Original-Struct ändern können.

```go
// Methode mit einem Wertempfänger definieren
func (r Rectangle) Umfang() float64 {
    return 2 * (r.Länge + r.Breite)
}

// Methode mit einem Zeigerempfänger definieren, der das Struct ändern kann
func (r *Rectangle) SetzeLänge(neueLänge float64) {
    r.Länge = neueLänge // kann den Wert des Original-Structs ändern
}

Im obigen Beispiel ist Umfang eine Methode mit einem Wertempfänger, wenn sie aufgerufen wird, ändert sich der Wert von Rechteck nicht. SetzeLänge ist jedoch eine Methode mit einem Zeigerempfänger, und der Aufruf dieser Methode wird die ursprüngliche Rechteck-Instanz beeinflussen.

6.3 Methodenaufruf

Sie können Methoden eines Structs mit der Struct-Variablen und ihrem Zeiger aufrufen.

func main() {
    rechteck := Rectangle{Länge: 10, Breite: 5}

    // Methode mit einem Wertempfänger aufrufen
    fmt.Println("Fläche:", rechteck.Fläche())

    // Methode mit einem Wertempfänger aufrufen
    fmt.Println("Umfang:", rechteck.Umfang())

    // Methode mit einem Zeigerempfänger aufrufen
    rechteck.SetzeLänge(20)

    // Methode mit einem Wertempfänger erneut aufrufen, beachten Sie, dass die Länge geändert wurde
    fmt.Println("Nach der Änderung, Fläche:", rechteck.Fläche())
}

Beim Aufrufen einer Methode mit einem Zeiger behandelt Go automatisch die Konvertierung zwischen Werten und Zeigern, unabhängig davon, ob Ihre Methode mit einem Wertempfänger oder einem Zeigerempfänger definiert ist.

6.4 Auswahl des Empfängertyps

Beim Definieren von Methoden sollten Sie basierend auf der Situation entscheiden, ob ein Wertempfänger oder ein Zeigerempfänger verwendet werden soll. Hier sind einige gängige Richtlinien:

  • Verwenden Sie einen Zeigerempfänger, wenn die Methode den Inhalt der Struktur ändern muss.
  • Verwenden Sie einen Zeigerempfänger, wenn die Struktur groß ist und die Kosten für das Kopieren hoch sind.
  • Verwenden Sie einen Zeigerempfänger, wenn Sie möchten, dass die Methode den Wert ändert, auf den der Empfänger zeigt.
  • Aus Effizienzgründen ist es selbst für den Fall, dass Sie die Struktur nicht ändern, sinnvoll, für eine große Struktur einen Zeigerempfänger zu verwenden.
  • Für kleine Strukturen oder wenn nur Daten gelesen werden, ohne dass eine Änderung erforderlich ist, ist ein Wertempfänger oft einfacher und effizienter.

Durch Strukturmethoden können in Go einige Funktionen der objektorientierten Programmierung wie Kapselung und Methoden simuliert werden. Dieser Ansatz in Go vereinfacht das Konzept der Objekte und bietet gleichzeitig ausreichend Möglichkeiten, verwandte Funktionen zu organisieren und zu verwalten.

7 Struktur und JSON-Serialisierung

In Go ist es oft erforderlich, eine Struktur in das JSON-Format für die Netzwerkübertragung oder als Konfigurationsdatei zu serialisieren. Ebenso müssen wir in der Lage sein, JSON in Strukturinstanzen zu deserialisieren. Das Paket encoding/json in Go bietet diese Funktionalität.

Hier ist ein Beispiel, wie man zwischen einer Struktur und JSON konvertiert:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// Definieren Sie die Person-Struktur und verwenden Sie JSON-Tags, um die Zuordnung zwischen den Strukturfeldern und den JSON-Feldnamen festzulegen
type Person struct {
	Name   string   `json:"name"`
	Alter  int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// Erstellen einer neuen Instanz von Person
	p := Person{
		Name:   "Max Mustermann",
		Alter:  30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// In JSON serialisieren
	jsonDaten, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("JSON-Serialisierung fehlgeschlagen: %s", err)
	}
	fmt.Printf("JSON-Format: %s\n", jsonDaten)

	// Deserialisieren in eine Struktur
	var p2 Person
	if err := json.Unmarshal(jsonDaten, &p2); err != nil {
		log.Fatalf("JSON-Deserialisierung fehlgeschlagen: %s", err)
	}
	fmt.Printf("Wiederhergestellte Struktur: %#v\n", p2)
}

In dem obigen Code haben wir eine Person-Struktur definiert, die ein Feld vom Typ Slice mit der "omitempty"-Option enthält. Diese Option besagt, dass das Feld nicht in das JSON aufgenommen wird, wenn es leer oder fehlend ist.

Wir haben die Funktion json.Marshal verwendet, um eine Strukturinstanz in JSON zu serialisieren, und die Funktion json.Unmarshal, um JSON-Daten in eine Strukturinstanz zu deserialisieren.

8 Fortgeschrittene Themen in Strukturen

8.1 Vergleich von Structs

In Go ist es erlaubt, zwei Instanzen von Structs direkt zu vergleichen, aber dieser Vergleich basiert auf den Werten der Felder innerhalb der Structs. Wenn alle Feldwerte gleich sind, werden die beiden Instanzen der Structs als gleich betrachtet. Es sollte beachtet werden, dass nicht alle Feldtypen verglichen werden können. Zum Beispiel können Structs, die Slices enthalten, nicht direkt verglichen werden.

Im Folgenden ist ein Beispiel für den Vergleich von Structs angegeben:

package main

import "fmt"

type Punkt struct {
	X, Y int
}

func main() {
	p1 := Punkt{1, 2}
	p2 := Punkt{1, 2}
	p3 := Punkt{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Ausgabe: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Ausgabe: p1 == p3: false
}

In diesem Beispiel werden p1 und p2 als gleich betrachtet, weil alle ihre Feldwerte gleich sind. Und p3 ist nicht gleich zu p1, weil der Wert von Y unterschiedlich ist.

8.2 Kopieren von Structs

In Go können Instanzen von Structs durch Zuweisung kopiert werden. Ob es sich bei dieser Kopie um eine tiefe Kopie oder eine oberflächliche Kopie handelt, hängt von den Typen der Felder innerhalb des Structs ab.

Wenn der Struct nur grundlegende Typen enthält (wie int, string, usw.), handelt es sich um eine tiefe Kopie. Enthält der Struct Referenztypen (wie Slices, Maps, usw.), so wird die Kopie eine oberflächliche Kopie sein, und die ursprüngliche Instanz und die neu kopierte Instanz teilen sich den Speicher der Referenztypen.

Im Folgenden ist ein Beispiel für das Kopieren eines Structs angegeben:

package main

import "fmt"

type Daten struct {
Zahlen []int
}

func main() {
// Initialisierung einer Instanz des Data Structs
original := Daten{Zahlen: []int{1, 2, 3}}

// Kopiere den Struct
kopiert := original

// Ändere die Elemente des kopierten Slices
kopiert.Zahlen[0] = 100

// Ansicht der Elemente der Original- und kopierten Instanzen
fmt.Println("Original:", original.Zahlen) // Ausgabe: Original: [100 2 3]
fmt.Println("Kopiert:", kopiert.Zahlen) // Ausgabe: Kopiert: [100 2 3]
}

Wie im Beispiel gezeigt, teilen sich die Instanzen von original und kopiert dieselbe Slice, daher wird das Modifizieren der Slicedaten in kopiert auch die Slicedaten in original beeinflussen.

Um dieses Problem zu vermeiden, kann echtes tiefes Kopieren erreicht werden, indem der Inhalt des Slices explizit in ein neues Slice kopiert wird:

newNumbers := make([]int, len(original.Zahlen))
copy(newNumbers, original.Zahlen)
kopiert := Daten{Zahlen: newNumbers}

Auf diese Weise werden Änderungen an kopiert nicht original beeinflussen.