Tiefer Eintauchgang in Go Struct
Daniel Hayes
Full-Stack Engineer · Leapcell

In Go ist struct
ein Aggregattyp, der zum Definieren und Einkapseln von Daten verwendet wird. Er ermöglicht die Kombination von Feldern unterschiedlicher Typen. Structs können als benutzerdefinierte Datentypen ähnlich wie Klassen in anderen Sprachen angesehen werden, unterstützen jedoch keine Vererbung. Methoden sind Funktionen, die einem bestimmten Typ (oft einem Struct) zugeordnet sind und über eine Instanz dieses Typs aufgerufen werden können.
Definieren und Initialisieren von Structs
Definieren eines Structs
Structs werden mit den Schlüsselwörtern type
und struct
definiert. Hier ist ein Beispiel für eine einfache Struct-Definition:
type User struct { Username string Email string SignInCount int IsActive bool }
Initialisieren eines Structs
Structs können auf verschiedene Arten initialisiert werden.
Initialisieren mit Feldnamen
user1 := User{ Username: "alice", Email: "alice@example.com", SignInCount: 1, IsActive: true, }
Initialisieren mit Standardwerten
Wenn einige Felder nicht angegeben werden, werden sie mit ihren Nullwerten für die jeweiligen Typen initialisiert.
user2 := User{ Username: "bob", }
In diesem Beispiel wird Email
mit einer leeren Zeichenkette (""
), SignInCount
mit 0
und IsActive
mit false
initialisiert.
Initialisieren mit einem Pointer
Ein Struct kann auch mit einem Pointer initialisiert werden.
user3 := &User{ Username: "charlie", Email: "charlie@example.com", }
Methoden und Verhalten von Structs
In Go dienen Structs nicht nur zum Speichern von Daten, sondern können auch Methoden haben, die für sie definiert sind. Dies ermöglicht es Structs, das Verhalten in Bezug auf ihre Daten zu kapseln. Im Folgenden finden Sie eine detaillierte Erläuterung der Struct-Methoden und des Verhaltens.
Definieren von Methoden für Structs
Methoden werden mit einem Receiver definiert, der der erste Parameter der Methode ist und den Typ angibt, zu dem die Methode gehört. Der Receiver kann entweder ein Value-Receiver oder ein Pointer-Receiver sein.
Value-Receiver
Ein Value-Receiver erstellt eine Kopie des Structs, wenn die Methode aufgerufen wird, sodass Änderungen an Feldern das ursprüngliche Struct nicht beeinflussen.
type User struct { Username string Email string } func (u User) PrintInfo() { fmt.Printf("Username: %s, Email: %s\n", u.Username, u.Email) }
Pointer-Receiver
Ein Pointer-Receiver ermöglicht es der Methode, die ursprünglichen Struct-Felder direkt zu ändern.
func (u *User) UpdateEmail(newEmail string) { u.Email = newEmail }
Methodensätze
In Go bilden alle Methoden eines Structs seinen Methodensatz. Der Methodensatz für einen Value-Receiver enthält alle Methoden mit Value-Receivern, während der Methodensatz für einen Pointer-Receiver alle Methoden mit Pointer- und Value-Receivern enthält.
Interfaces und Struct-Methoden
Struct-Methoden werden oft mit Interfaces verwendet, um Polymorphismus zu erreichen. Beim Definieren eines Interfaces geben Sie die Methoden an, die ein Struct implementieren muss.
type UserInfo interface { PrintInfo() } // User implementiert das UserInfo Interface func (u User) PrintInfo() { fmt.Printf("Username: %s, Email: %s\n", u.Username, u.Email) } func ShowInfo(ui UserInfo) { ui.PrintInfo() }
Speicherausrichtung in Structs
In Go ist die Speicherausrichtung für Structs darauf ausgelegt, die Zugriffseffizienz zu verbessern. Verschiedene Datentypen haben spezifische Ausrichtungsanforderungen, und der Compiler kann zwischen Struct-Feldern Padding-Bytes einfügen, um diese Anforderungen zu erfüllen.
Was ist Speicherausrichtung?
Speicherausrichtung bedeutet, dass Daten im Speicher sich an Adressen befinden müssen, die Vielfache bestimmter Werte sind. Die Größe eines Datentyps bestimmt seine Ausrichtungsanforderung. Beispielsweise benötigt int32
eine Ausrichtung auf 4 Bytes und int64
eine Ausrichtung auf 8 Bytes.
Warum ist Speicherausrichtung notwendig?
Ein effizienter Speicherzugriff ist entscheidend für die CPU-Leistung. Wenn eine Variable nicht richtig ausgerichtet ist, benötigt die CPU möglicherweise mehrere Speicherzugriffe, um Daten zu lesen oder zu schreiben, was zu Leistungseinbußen führt. Durch die Ausrichtung von Daten stellt der Compiler einen effizienten Speicherzugriff sicher.
Regeln für die Struct-Speicherausrichtung
- Feldausrichtung: Die Adresse jedes Feldes muss die Ausrichtungsanforderungen seines Typs erfüllen. Der Compiler kann zwischen Feldern Padding-Bytes einfügen, um eine ordnungsgemäße Ausrichtung zu gewährleisten.
- Struct-Ausrichtung: Die Größe eines Structs muss ein Vielfaches der größten Ausrichtungsanforderung unter seinen Feldern sein.
Beispiel:
package main import ( "fmt" "unsafe" ) type Example struct { a int8 // 1 Byte b int32 // 4 Bytes c int8 // 1 Byte } func main() { fmt.Println(unsafe.Sizeof(Example{})) }
Ausgabe: 12
Analyse:
a
istint8
und belegt 1 Byte, ausgerichtet auf 1.b
istint32
und benötigt eine Ausrichtung auf 4 Bytes. Der Compiler fügt zwischena
undb
3 Padding-Bytes ein, um die Adresse vonb
auf 4 auszurichten.c
istint8
und benötigt 1 Byte, aber die Gesamtgröße des Structs muss ein Vielfaches von 4 sein (die größte Ausrichtungsanforderung). Der Compiler fügt am Ende 3 Padding-Bytes hinzu.
Optimieren der Speicherausrichtung
Sie können Struct-Felder neu anordnen, um das Padding zu minimieren und den Speicherverbrauch zu reduzieren.
type Optimized struct { b int32 // 4 Bytes a int8 // 1 Byte c int8 // 1 Byte }
Ausgabe: 8
In dieser optimierten Version wird b
zuerst platziert und auf 4 Bytes ausgerichtet. a
und c
werden nacheinander platziert, wodurch die Gesamtgröße 8 Bytes beträgt, was kompakter ist als die nicht optimierte Version.
Zusammenfassung
- Struct-Felder in Go werden basierend auf ihren Ausrichtungsanforderungen Speicher zugewiesen, mit potenziellen Padding-Bytes.
- Das Anpassen der Reihenfolge der Felder kann das Padding minimieren und die Speichernutzung optimieren.
- Verwenden Sie
unsafe.Sizeof
, um die tatsächliche Speichergröße eines Structs zu bestimmen.
Verschachtelte Structs und Komposition
In Go sind verschachtelte Structs und Komposition leistungsstarke Werkzeuge für die Wiederverwendung von Code und die Organisation komplexer Daten. Verschachtelte Structs ermöglichen es einem Struct, ein anderes Struct als Feld einzubeziehen, wodurch die Erstellung komplexer Datenmodelle ermöglicht wird. Komposition hingegen erstellt neue Structs, indem sie andere Structs einbezieht, was die Wiederverwendung von Code erleichtert.
Verschachtelte Structs
Verschachtelte Structs ermöglichen es einem Struct, ein anderes Struct als Feld einzubeziehen. Dies macht Datenstrukturen flexibler und organisierter. Hier ist ein Beispiel für ein verschachteltes Struct:
package main import "fmt" // Definiere das Address Struct type Address struct { City string Country string } // Definiere das User Struct, welches das Address Struct beinhaltet type User struct { Username string Email string Address Address // Verschachteltes Struct } func main() { // Initialisiere das verschachtelte Struct user := User{ Username: "alice", Email: "alice@example.com", Address: Address{ City: "New York", Country: "USA", }, } // Greife auf die Felder des verschachtelten Structs zu fmt.Printf("User: %s, Email: %s, City: %s, Country: %s\n", user.Username, user.Email, user.Address.City, user.Address.Country) }
Struct Komposition
Die Komposition ermöglicht es, mehrere Structs zu einem neuen Struct zu kombinieren, was die Wiederverwendung von Code ermöglicht. Bei der Komposition kann ein Struct mehrere andere Structs als Felder enthalten. Dies hilft beim Aufbau komplexerer Modelle und beim Austausch gemeinsamer Felder oder Methoden. Hier ist ein Beispiel für Struct-Komposition:
package main import "fmt" // Definiere das Address Struct type Address struct { City string Country string } // Definiere das Profile Struct type Profile struct { Age int Bio string } // Definiere das User Struct, welches Address und Profile komponiert type User struct { Username string Email string Address Address // Komponiert das Address Struct Profile Profile // Komponiert das Profile Struct } func main() { // Initialisiere das komponierte Struct user := User{ Username: "bob", Email: "bob@example.com", Address: Address{ City: "New York", Country: "USA", }, Profile: Profile{ Age: 25, Bio: "Ein Softwareentwickler.", }, } // Greife auf die Felder des komponierten Structs zu fmt.Printf("User: %s, Email: %s, City: %s, Age: %d, Bio: %s\n", user.Username, user.Email, user.Address.City, user.Profile.Age, user.Profile.Bio) }
Unterschiede zwischen verschachtelten Structs und Komposition
- Verschachtelte Structs: Werden verwendet, um Structs zusammenzufügen, wobei der Typ eines Feldes in einem Struct ein anderes Struct ist. Dieser Ansatz wird oft verwendet, um Datenmodelle mit hierarchischen Beziehungen zu beschreiben.
- Komposition: Ermöglicht es einem Struct, Felder aus mehreren anderen Structs einzubeziehen. Diese Methode wird verwendet, um die Wiederverwendung von Code zu erreichen, wodurch ein Struct komplexere Verhaltensweisen und Attribute haben kann.
Zusammenfassung
Verschachtelte Structs und Komposition sind leistungsstarke Funktionen in Go, die helfen, komplexe Datenstrukturen zu organisieren und zu verwalten. Bei der Gestaltung von Datenmodellen kann die angemessene Verwendung von verschachtelten Structs und Komposition Ihren Code übersichtlicher und wartungsfreundlicher machen.
Leerer Struct
Ein leerer Struct in Go ist ein Struct ohne Felder.
Größe und Speicheradresse
Ein leerer Struct belegt null Byte Speicher. Seine Speicheradresse kann jedoch unter verschiedenen Umständen gleich sein oder auch nicht. Wenn ein Memory-Escape auftritt, sind die Adressen gleich und verweisen auf runtime.zerobase
.
// empty_struct.go type Empty struct{} //go:linkname zerobase runtime.zerobase var zerobase uintptr // Verwenden der go:linkname-Direktive, um zerobase mit runtime.zerobase zu verknüpfen func main() { a := Empty{} b := struct{}{} fmt.Println(unsafe.Sizeof(a) == 0) // true fmt.Println(unsafe.Sizeof(b) == 0) // true fmt.Printf("%p\n", &a) // 0x590d00 fmt.Printf("%p\n", &b) // 0x590d00 fmt.Printf("%p\n", &zerobase) // 0x590d00 c := new(Empty) d := new(Empty) // Erzwingt, dass c und d escapen fmt.Sprint(c, d) println(c) // 0x590d00 println(d) // 0x590d00 fmt.Println(c == d) // true e := new(Empty) f := new(Empty) println(e) // 0xc00008ef47 println(f) // 0xc00008ef47 fmt.Println(e == f) // false }
Aus der Ausgabe geht hervor, dass die Variablen a
, b
und zerobase
dieselbe Adresse haben und alle auf die globale Variable runtime.zerobase
(runtime/malloc.go
) verweisen.
Bezüglich Escape-Szenarien:
- Die Variablen
c
undd
entkommen in den Heap. Ihre Adressen sind0x590d00
, und sie werden als gleich verglichen (true
). - Die Variablen
e
undf
haben unterschiedliche Adressen (0xc00008ef47
) und werden als ungleich verglichen (false
).
Dieses Verhalten ist in Go beabsichtigt. Wenn leere Struct-Variablen nicht entkommen, sind ihre Pointer ungleich. Nach dem Entkommen werden die Pointer gleich.
Speicherberechnung beim Einbetten leerer Structs
Ein leerer Struct selbst belegt keinen Speicherplatz, aber wenn er in einen anderen Struct eingebettet ist, kann er je nach Position Speicherplatz verbrauchen:
- Wenn er das einzige Feld in dem Struct ist, belegt der Struct keinen Speicherplatz.
- Wenn er das erste oder ein Zwischenfeld ist, belegt er keinen Speicherplatz.
- Wenn er das letzte Feld ist, belegt er Speicherplatz in Höhe des vorherigen Feldes.
type s1 struct { a struct{} } type s2 struct { _ struct{} } type s3 struct { a struct{} b byte } type s4 struct { a struct{} b int64 } type s5 struct { a byte b struct{} c int64 } type s6 struct { a byte b struct{} } type s7 struct { a int64 b struct{} } type s8 struct { a struct{} b struct{} } func main() { fmt.Println(unsafe.Sizeof(s1{})) // 0 fmt.Println(unsafe.Sizeof(s2{})) // 0 fmt.Println(unsafe.Sizeof(s3{})) // 1 fmt.Println(unsafe.Sizeof(s4{})) // 8 fmt.Println(unsafe.Sizeof(s5{})) // 16 fmt.Println(unsafe.Sizeof(s6{})) // 2 fmt.Println(unsafe.Sizeof(s7{})) // 16 fmt.Println(unsafe.Sizeof(s8{})) // 0 }
Wenn leere Structs Elemente von Arrays oder Slices sind:
var a [10]int fmt.Println(unsafe.Sizeof(a)) // 80 var b [10]struct{} fmt.Println(unsafe.Sizeof(b)) // 0 var c = make([]struct{}, 10) fmt.Println(unsafe.Sizeof(c)) // 24, die Größe des Slice-Headers
Anwendungen
Die Nullgrößen-Eigenschaft leerer Structs ermöglicht es, sie für verschiedene Zwecke ohne zusätzlichen Speicheraufwand zu verwenden.
Verhindern der nicht mit Schlüssel versehenen Struct-Initialisierung
type MustKeyedStruct struct { Name string Age int _ struct{} } func main() { person := MustKeyedStruct{Name: "hello", Age: 10} fmt.Println(person) person2 := MustKeyedStruct{"hello", 10} // Kompilierungsfehler: zu wenige Werte in MustKeyedStruct{...} fmt.Println(person2) }
Implementieren einer Set-Datenstruktur
package main import ( "fmt" ) type Set struct { items map[interface{}]emptyItem } type emptyItem struct{} var itemExists = emptyItem{} func NewSet() *Set { return &Set{items: make(map[interface{}]emptyItem)} } func (set *Set) Add(item interface{}) { set.items[item] = itemExists } func (set *Set) Remove(item interface{}) { delete(set.items, item) } func (set *Set) Contains(item interface{}) bool { _, contains := set.items[item] return contains } func (set *Set) Size() int { return len(set.items) } func main() { set := NewSet() set.Add("hello") set.Add("world") fmt.Println(set.Contains("hello")) fmt.Println(set.Contains("Hello")) fmt.Println(set.Size()) }
Signalübertragung über Channels
Manchmal ist der Inhalt der über einen Channel übertragenen Daten irrelevant und dient nur als Signal. Beispielsweise können leere Structs in Semaphor-Implementierungen verwendet werden:
var empty = struct{}{} type Semaphore chan struct{} func (s Semaphore) P(n int) { for i := 0; i < n; i++ { s <- empty } } func (s Semaphore) V(n int) { for i := 0; i < n; i++ { <-s } } func (s Semaphore) Lock() { s.P(1) } func (s Semaphore) Unlock() { s.V(1) } func NewSemaphore(N int) Semaphore { return make(Semaphore, N) }
Wir sind Leapcell, Ihre erste Wahl für die Bereitstellung von Go-Projekten in der Cloud.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
- Mehrsprachige Unterstützung
- Entwickeln Sie mit JavaScript, Python, Go oder Rust.
- Stellen Sie unbegrenzt Projekte kostenlos bereit
- zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
- Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 US-Dollar unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
- Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für umsetzbare Erkenntnisse.
- Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur einfachen Bewältigung hoher Parallelität.
- Kein Betriebsaufwand - konzentrieren Sie sich einfach auf den Aufbau.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ