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.