Go: Zeiger und Speicherverwaltung
TL;DR: Entdecken Sie die Speicherverwaltung von Go mit Zeigern, Stapel- und Heap-Zuweisungen, Escape-Analyse und Speicherbereinigung anhand von Beispielen
Als ich anfing, Go zu lernen, war ich von seinem Ansatz zur Speicherverwaltung fasziniert, insbesondere wenn es um Zeiger ging. Go verwaltet den Speicher auf eine Weise, die sowohl effizient als auch sicher ist, aber es kann eine Art Black Box sein, wenn Sie nicht unter die Haube blicken. Ich möchte einige Einblicke darüber geben, wie Go den Speicher mit Zeigern, dem Stapel und dem Heap sowie Konzepten wie Escape-Analyse und Speicherbereinigung verwaltet. Unterwegs schauen wir uns Codebeispiele an, die diese Ideen in der Praxis veranschaulichen.
Stapel- und Heapspeicher verstehen
Bevor Sie sich mit Zeigern in Go befassen, ist es hilfreich zu verstehen, wie der Stapel und der Heap funktionieren. Dies sind zwei Speicherbereiche, in denen Variablen mit jeweils eigenen Eigenschaften gespeichert werden können.
- Stapel: Dies ist ein Speicherbereich, der nach dem Last-In-First-Out-Prinzip arbeitet. Es ist schnell und effizient und wird zum Speichern von Variablen mit kurzlebigem Gültigkeitsbereich verwendet, wie z. B. lokale Variablen innerhalb von Funktionen.
- Heap: Dies ist ein größerer Speicherpool, der für Variablen verwendet wird, die über den Bereich einer Funktion hinausgehen müssen, wie z. B. Daten, die von einer Funktion zurückgegeben und an anderer Stelle verwendet werden.
In Go entscheidet der Compiler basierend auf ihrer Verwendung, ob Variablen auf dem Stack oder dem Heap zugewiesen werden. Dieser Entscheidungsprozess wird Fluchtanalyse genannt, auf den wir später noch näher eingehen werden.
Wertübergabe: Das Standardverhalten
Wenn Sie in Go Variablen wie Ganzzahlen, Zeichenfolgen oder boolesche Werte an eine Funktion übergeben, werden diese natürlich als Wert übergeben. Das bedeutet, dass eine Kopie der Variablen erstellt wird und die Funktion mit dieser Kopie arbeitet. Das bedeutet, dass jede an der Variablen innerhalb der Funktion vorgenommene Änderung keine Auswirkungen auf die Variable außerhalb ihres Gültigkeitsbereichs hat.
Hier ist ein einfaches Beispiel:
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Ausgabe:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
In diesem Code:
- Die Funktion increment() erhält eine Kopie von n.
- Die Adressen von n in main() und num in inkrement() sind unterschiedlich.
- Das Ändern von num in increment() hat keinen Einfluss auf n in main().
Takeaway: Die Übergabe von Werten ist sicher und unkompliziert, aber bei großen Datenstrukturen kann das Kopieren ineffizient werden.
Einführung in Zeiger: Übergabe als Referenz
Um die ursprüngliche Variable innerhalb einer Funktion zu ändern, können Sie einen Zeiger darauf übergeben. Ein Zeiger enthält die Speicheradresse einer Variablen und ermöglicht es Funktionen, auf die Originaldaten zuzugreifen und diese zu ändern.
So können Sie Zeiger verwenden:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Ausgabe:
Before incrementPointer(): n = 42, address = 0xc00009a040 Inside incrementPointer(): num = 43, address = 0xc00009a040 After incrementPointer(): n = 43, address = 0xc00009a040
In diesem Beispiel:
- Wir übergeben die Adresse von n an incrementPointer().
- Sowohl main() als auch incrementPointer() beziehen sich auf dieselbe Speicheradresse.
- Das Ändern von num in incrementPointer() wirkt sich auf n in main() aus.
Takeaway: Die Verwendung von Zeigern ermöglicht es Funktionen, die ursprüngliche Variable zu ändern, führt jedoch zu Überlegungen zur Speicherzuweisung.
Speicherzuweisung mit Zeigern
Wenn Sie einen Zeiger auf eine Variable erstellen, muss Go sicherstellen, dass die Variable so lange lebt wie der Zeiger. Dies bedeutet oft, dass die Variable auf dem Heap und nicht auf dem Stack.
zugewiesen wirdBedenken Sie diese Funktion:
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Hier ist num eine lokale Variable innerhalb von createPointer(). Wenn num auf dem Stapel gespeichert wäre, würde es bereinigt, sobald die Funktion zurückkehrt, und es würde ein hängender Zeiger zurückbleiben. Um dies zu verhindern, weist Go num auf dem Heap zu, sodass dieser nach dem Beenden von createPointer() gültig bleibt.
baumelnde Zeiger
Ein baumelnder Zeiger tritt auf, wenn ein Zeiger auf Speicher verweist, der bereits freigegeben wurde.
Go verhindert baumelnde Zeiger mit seinem Garbage Collector und stellt so sicher, dass kein Speicher freigegeben wird, solange noch auf ihn verwiesen wird. Das Festhalten an Zeigern länger als nötig kann jedoch in bestimmten Szenarien zu einer erhöhten Speichernutzung oder Speicherlecks führen.
Escape-Analyse: Entscheidung über Stack- vs. Heap-Zuordnung
Escape-Analyse bestimmt, ob Variablen über ihren Funktionsumfang hinaus leben müssen. Wenn eine Variable zurückgegeben, in einem Zeiger gespeichert oder von einer Goroutine erfasst wird, wird sie maskiert und auf dem Heap zugewiesen. Selbst wenn eine Variable jedoch nicht maskiert wird, kann es sein, dass der Compiler sie aus anderen Gründen auf dem Heap zuordnet, beispielsweise aufgrund von Optimierungsentscheidungen oder Beschränkungen der Stapelgröße.
Beispiel für ein Variablen-Escape:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
In diesem Code:
- Die Slice-Daten in createSlice() werden maskiert, da sie in main() zurückgegeben und verwendet werden.
- Das zugrunde liegende Array des Slice wird auf dem Heap zugeordnet.
Escape-Analyse mit go build -gcflags '-m' verstehen
Sie können sehen, was der Compiler von Go entscheidet, indem Sie die Option -gcflags '-m' verwenden:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Dadurch werden Meldungen ausgegeben, die angeben, ob Variablen auf den Heap entkommen.
Garbage Collection in Go
Go verwendet einen Garbage Collector, um die Speicherzuweisung und -freigabe auf dem Heap zu verwalten. Es gibt automatisch Speicher frei, auf den nicht mehr verwiesen wird, und trägt so dazu bei, Speicherlecks zu verhindern.
Beispiel:
Before incrementPointer(): n = 42, address = 0xc00009a040 Inside incrementPointer(): num = 43, address = 0xc00009a040 After incrementPointer(): n = 43, address = 0xc00009a040
In diesem Code:
- Wir erstellen eine verknüpfte Liste mit 1.000.000 Knoten.
- Jeder Knoten wird auf dem Heap zugewiesen, da er dem Gültigkeitsbereich von createLinkedList() entgeht.
- Der Garbage Collector gibt den Speicher frei, wenn die Liste nicht mehr benötigt wird.
Takeaway: Der Garbage Collector von Go vereinfacht die Speicherverwaltung, kann aber zu Mehraufwand führen.
Mögliche Fallstricke bei Zeigern
Obwohl Hinweise wirkungsvoll sind, können sie zu Problemen führen, wenn sie nicht sorgfältig verwendet werden.
Baumelnde Zeiger (Fortsetzung)
Obwohl der Garbage Collector von Go dabei hilft, baumelnde Zeiger zu verhindern, können dennoch Probleme auftreten, wenn Sie Zeiger länger als nötig behalten.
Beispiel:
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
In diesem Code:
- Daten sind ein großes Stück, das auf dem Heap zugewiesen wird.
- Indem wir einen Verweis darauf behalten ([]int), verhindern wir, dass der Garbage Collector den Speicher freigibt.
- Dies kann zu einer erhöhten Speichernutzung führen, wenn es nicht richtig verwaltet wird.
Parallelitätsprobleme – Datenwettlauf mit Zeigern
Hier ist ein Beispiel, bei dem Zeiger direkt beteiligt sind:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Warum dieser Code fehlschlägt:
- Mehrere Goroutinen dereferenzieren und erhöhen den Zeiger counterPtr ohne Synchronisierung.
- Dies führt zu einem Datenwettlauf, da mehrere Goroutinen ohne Synchronisierung gleichzeitig auf denselben Speicherort zugreifen und ihn ändern. Die Operation *counterPtr umfasst mehrere Schritte (Lesen, Inkrementieren, Schreiben) und ist nicht threadsicher.
Behebung des Datenwettlaufs:
Wir können dies beheben, indem wir die Synchronisierung mit einem Mutex hinzufügen:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
So funktioniert dieser Fix:
- mu.Lock() und mu.Unlock() stellen sicher, dass jeweils nur eine Goroutine auf den Zeiger zugreift und ihn ändert.
- Dies verhindert Race Conditions und stellt sicher, dass der Endwert des Zählers korrekt ist.
Was sagt die Sprachspezifikation von Go?
Es ist erwähnenswert, dass die Sprachspezifikation von Go nicht direkt vorschreibt, ob Variablen auf dem Stapel oder dem Heap zugewiesen werden. Hierbei handelt es sich um Laufzeit- und Compiler-Implementierungsdetails, die Flexibilität und Optimierungen ermöglichen, die je nach Go-Version oder Implementierung variieren können.
Das bedeutet:
- Die Art und Weise, wie der Speicher verwaltet wird, kann sich zwischen verschiedenen Go-Versionen ändern.
- Sie sollten sich nicht darauf verlassen, dass Variablen in einem bestimmten Speicherbereich zugewiesen werden.
- Konzentrieren Sie sich darauf, klaren und korrekten Code zu schreiben, anstatt zu versuchen, die Speicherzuweisung zu kontrollieren.
Beispiel:
Selbst wenn Sie erwarten, dass eine Variable auf dem Stapel zugewiesen wird, kann der Compiler aufgrund seiner Analyse entscheiden, sie auf den Heap zu verschieben.
package main import "fmt" func increment(num int) { num++ fmt.Printf("Inside increment(): num = %d, address = %p \n", num, &num) } func main() { n := 21 fmt.Printf("Before increment(): n = %d, address = %p \n", n, &n) increment(n) fmt.Printf("After increment(): n = %d, address = %p \n", n, &n) }
Takeaway: Da es sich bei den Speicherzuteilungsdetails eher um eine interne Implementierung und nicht um Teil der Go-Sprachspezifikation handelt, handelt es sich bei diesen Informationen nur um allgemeine Richtlinien und nicht um feste Regeln, die sich zu einem späteren Zeitpunkt ändern können.
Ausbalancieren von Leistung und Speichernutzung
Bei der Entscheidung zwischen der Übergabe per Wert oder per Zeiger müssen wir die Größe der Daten und die Auswirkungen auf die Leistung berücksichtigen.
Übergabe großer Strukturen nach Wert:
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Übergabe großer Strukturen per Zeiger:
package main import "fmt" func incrementPointer(num *int) { (*num)++ fmt.Printf("Inside incrementPointer(): num = %d, address = %p \n", *num, num) } func main() { n := 42 fmt.Printf("Before incrementPointer(): n = %d, address = %p \n", n, &n) incrementPointer(&n) fmt.Printf("After incrementPointer(): n = %d, address = %p \n", n, &n) }
Überlegungen:
- Die Wertübergabe ist sicher und unkompliziert, kann jedoch bei großen Datenstrukturen ineffizient sein.
- Die Übergabe des Zeigers vermeidet das Kopieren, erfordert jedoch eine sorgfältige Handhabung, um Parallelitätsprobleme zu vermeiden.
Aus der Praxiserfahrung:
Zu Beginn meiner Karriere erinnerte ich mich an eine Zeit, als ich eine Go-Anwendung optimierte, die große Datenmengen verarbeitete. Anfangs habe ich große Strukturen als Wert übergeben, in der Annahme, dass dies die Argumentation über den Code vereinfachen würde. Allerdings sind mir zufällig eine vergleichsweise hohe Speicherauslastung und häufige Pausen bei der Garbage Collection aufgefallen.
Nachdem wir in einer Paarprogrammierung mit meinem Vorgesetzten ein Profil der Anwendung mit dem pprof-Tool von Go erstellt hatten, stellten wir fest, dass das Kopieren großer Strukturen einen Engpass darstellte. Wir haben den Code umgestaltet, um Zeiger anstelle von Werten zu übergeben. Dadurch wurde der Speicherverbrauch reduziert und die Leistung erheblich verbessert.
Aber die Veränderung verlief nicht ohne Herausforderungen. Wir mussten sicherstellen, dass unser Code Thread-sicher ist, da nun mehrere Goroutinen auf gemeinsam genutzte Daten zugreifen. Wir haben die Synchronisierung mithilfe von Mutexes implementiert und den Code sorgfältig auf mögliche Rennbedingungen überprüft.
Lesson Learned: Ein sehr frühes Verständnis darüber, wie Go mit der Speicherzuweisung umgeht, kann Ihnen dabei helfen, effizienteren Code zu schreiben, da es wichtig ist, Leistungssteigerungen mit Codesicherheit und Wartbarkeit in Einklang zu bringen.
Letzte Gedanken
Gos Ansatz zur Speicherverwaltung schafft (wie überall sonst auch) ein Gleichgewicht zwischen Leistung und Einfachheit. Durch die Abstrahierung vieler Low-Level-Details können sich Entwickler auf die Erstellung robuster Anwendungen konzentrieren, ohne sich in der manuellen Speicherverwaltung zu verzetteln.
Wichtige Punkte, die Sie beachten sollten:
- Wertübergabe ist einfach, kann aber bei großen Datenstrukturen ineffizient sein.
- Die Verwendung von Zeigern kann die Leistung verbessern, erfordert jedoch eine sorgfältige Handhabung, um Probleme wie Datenrennen zu vermeiden.
- Escape-Analyse bestimmt, ob Variablen auf dem Stapel oder Heap zugewiesen werden, aber das ist ein internes Detail.
- Garbage Collection hilft, Speicherlecks zu verhindern, kann aber zu Mehraufwand führen.
- Parallelität erfordert eine Synchronisierung, wenn es um gemeinsam genutzte Daten geht.
Wenn Sie diese Konzepte im Hinterkopf behalten und die Tools von Go zum Profilieren und Analysieren Ihres Codes verwenden, können Sie effiziente und sichere Anwendungen schreiben.
Ich hoffe, dass diese Untersuchung der Speicherverwaltung von Go mit Hinweisen hilfreich sein wird. Egal, ob Sie gerade erst mit Go beginnen oder Ihr Verständnis vertiefen möchten: Das Experimentieren mit Code und das Beobachten des Compiler- und Laufzeitverhaltens ist eine großartige Möglichkeit zum Lernen.
Zögern Sie nicht, Ihre Erfahrungen oder Fragen zu teilen – ich bin immer daran interessiert, mehr über Go! zu diskutieren, zu lernen und zu schreiben.
Bonusinhalt – Direct Pointer-Unterstützung
Weißt du? Zeiger können für bestimmte Datentypen direkt erstellt werden, für einige jedoch nicht. Diese kurze Tabelle deckt sie ab.
Type | Supports Direct Pointer Creation? | Example |
---|---|---|
Structs | ✅ Yes | p := &Person{Name: "Alice", Age: 30} |
Arrays | ✅ Yes | arrPtr := &[3]int{1, 2, 3} |
Slices | ❌ No (indirect via variable) | slice := []int{1, 2, 3}; slicePtr := &slice |
Maps | ❌ No (indirect via variable) | m := map[string]int{}; mPtr := &m |
Channels | ❌ No (indirect via variable) | ch := make(chan int); chPtr := &ch |
Basic Types | ❌ No (requires a variable) | val := 42; p := &val |
time.Time (Struct) | ✅ Yes | t := &time.Time{} |
Custom Structs | ✅ Yes | point := &Point{X: 1, Y: 2} |
Interface Types | ✅ Yes (but rarely needed) | var iface interface{} = "hello"; ifacePtr := &iface |
time.Duration (Alias of int64) | ❌ No | duration := time.Duration(5); p := &duration |
Bitte lassen Sie mich in den Kommentaren wissen, ob Ihnen das gefällt; Ich werde versuchen, in Zukunft solche Bonusinhalte zu meinen Artikeln hinzuzufügen.
Danke fürs Lesen! Weitere Inhalte finden Sie hier.
Möge der Code mit dir sein :)
Meine sozialen Links: LinkedIn | GitHub | ? (ehemals Twitter) | Unterstapel | Dev.to | Hashnode
Das obige ist der detaillierte Inhalt vonGo: Zeiger und Speicherverwaltung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Heiße KI -Werkzeuge

Undresser.AI Undress
KI-gestützte App zum Erstellen realistischer Aktfotos

AI Clothes Remover
Online-KI-Tool zum Entfernen von Kleidung aus Fotos.

Undress AI Tool
Ausziehbilder kostenlos

Clothoff.io
KI-Kleiderentferner

Video Face Swap
Tauschen Sie Gesichter in jedem Video mühelos mit unserem völlig kostenlosen KI-Gesichtstausch-Tool aus!

Heißer Artikel

Heiße Werkzeuge

Notepad++7.3.1
Einfach zu bedienender und kostenloser Code-Editor

SublimeText3 chinesische Version
Chinesische Version, sehr einfach zu bedienen

Senden Sie Studio 13.0.1
Leistungsstarke integrierte PHP-Entwicklungsumgebung

Dreamweaver CS6
Visuelle Webentwicklungstools

SublimeText3 Mac-Version
Codebearbeitungssoftware auf Gottesniveau (SublimeText3)

Heiße Themen











Golang ist in Bezug auf Leistung und Skalierbarkeit besser als Python. 1) Golangs Kompilierungseigenschaften und effizientes Parallelitätsmodell machen es in hohen Parallelitätsszenarien gut ab. 2) Python wird als interpretierte Sprache langsam ausgeführt, kann aber die Leistung durch Tools wie Cython optimieren.

Golang ist in Gleichzeitigkeit besser als C, während C bei Rohgeschwindigkeit besser als Golang ist. 1) Golang erreicht durch Goroutine und Kanal eine effiziente Parallelität, die zum Umgang mit einer großen Anzahl von gleichzeitigen Aufgaben geeignet ist. 2) C über Compiler -Optimierung und Standardbibliothek bietet es eine hohe Leistung in der Nähe der Hardware, die für Anwendungen geeignet ist, die eine extreme Optimierung erfordern.

GoisidealforBeginersandSuitableforCloudandNetWorkServicesDuetoitsSimplicity, Effizienz und Konsumfeaturen.1) InstallgoFromTheofficialwebSiteAnDverifyWith'goversion'.2) CreateAneDrunyourFirstProgramwith'gorunhello.go.go.go.

Golang ist für schnelle Entwicklung und gleichzeitige Szenarien geeignet, und C ist für Szenarien geeignet, in denen extreme Leistung und Kontrolle auf niedriger Ebene erforderlich sind. 1) Golang verbessert die Leistung durch Müllsammlung und Parallelitätsmechanismen und eignet sich für die Entwicklung von Webdiensten mit hoher Konsequenz. 2) C erreicht die endgültige Leistung durch das manuelle Speicherverwaltung und die Compiler -Optimierung und eignet sich für eingebettete Systementwicklung.

Golang und Python haben jeweils ihre eigenen Vorteile: Golang ist für hohe Leistung und gleichzeitige Programmierung geeignet, während Python für Datenwissenschaft und Webentwicklung geeignet ist. Golang ist bekannt für sein Parallelitätsmodell und seine effiziente Leistung, während Python für sein Ökosystem für die kurze Syntax und sein reiches Bibliothek bekannt ist.

Die Leistungsunterschiede zwischen Golang und C spiegeln sich hauptsächlich in der Speicherverwaltung, der Kompilierungsoptimierung und der Laufzeiteffizienz wider. 1) Golangs Müllsammlung Mechanismus ist praktisch, kann jedoch die Leistung beeinflussen.

Golang und C haben jeweils ihre eigenen Vorteile bei Leistungswettbewerben: 1) Golang ist für eine hohe Parallelität und schnelle Entwicklung geeignet, und 2) C bietet eine höhere Leistung und eine feinkörnige Kontrolle. Die Auswahl sollte auf Projektanforderungen und Teamtechnologie -Stack basieren.

GolangissidealforbuildingsCalablesSystemduetoitseffizienz und Konsumverkehr, whilepythonexcelsinquickScriptingandDataanalyseduetoitssimplication und VacevastEcosystem.golangsDesineScouragesCouragescournations, tadelcodedeanDitsGoroutaTinoutgoroutaTinoutgoroutaTinoutsGoroutinesGoroutinesGoroutsGoroutins, t
