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.
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.
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.
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:
Takeaway: Die Übergabe von Werten ist sicher und unkompliziert, aber bei großen Datenstrukturen kann das Kopieren ineffizient werden.
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:
Takeaway: Die Verwendung von Zeigern ermöglicht es Funktionen, die ursprüngliche Variable zu ändern, führt jedoch zu Überlegungen zur Speicherzuweisung.
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 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:
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.
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:
Takeaway: Der Garbage Collector von Go vereinfacht die Speicherverwaltung, kann aber zu Mehraufwand führen.
Obwohl Hinweise wirkungsvoll sind, können sie zu Problemen führen, wenn sie nicht sorgfältig verwendet werden.
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:
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:
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:
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:
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.
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:
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.
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:
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.
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!