Go: Zeiger und Speicherverwaltung

Patricia Arquette
Freigeben: 2024-11-22 01:51:14
Original
438 Leute haben es durchsucht

Go: Pointers & Memory Management

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)
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ausgabe:

Before increment(): n = 21, address = 0xc000012070 
Inside increment(): num = 22, address = 0xc000012078 
After increment(): n = 21, address = 0xc000012070
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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)
}

Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ausgabe:

Before incrementPointer(): n = 42, address = 0xc00009a040 
Inside incrementPointer(): num = 43, address = 0xc00009a040 
After incrementPointer(): n = 43, address = 0xc00009a040 
Nach dem Login kopieren
Nach dem Login kopieren

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 wird

Bedenken 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)
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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)
}

Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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 
Nach dem Login kopieren
Nach dem Login kopieren

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)
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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)
}

Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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)
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ü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)
}

Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

Ü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
Typ Unterstützt die direkte Zeigererstellung? Beispiel Strukturen ✅ Ja p := &Person{Name: "Alice", Alter: 30 Arrays ✅ Ja arrPtr := &[3]int{1, 2, 3} Scheiben ❌ Nein (indirekt über Variable) slice := []int{1, 2, 3}; SlicePtr := &slice Karten ❌ Nein (indirekt über Variable) m := map[string]int{}; mPtr := &m Kanäle ❌ Nein (indirekt über Variable) ch := make(chan int); chPtr := &ch Grundtypen ❌ Nein (erfordert eine Variable) val := 42; p := &val time.Time (Struktur) ✅ Ja t := &time.Time{} Benutzerdefinierte Strukturen ✅ Ja point := &Point{X: 1, Y: 2} Schnittstellentypen ✅ Ja (wird aber selten benötigt) var iface interface{} = "hello"; ifacePtr := &iface time.Duration (Alias ​​von int64) ❌ Nein 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!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Neueste Artikel des Autors
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage