TL;DR : Explorez la gestion de la mémoire de Go avec des pointeurs, des allocations de pile et de tas, une analyse d'échappement et un garbage collection avec des exemples
Quand j'ai commencé à apprendre Go, j'ai été intrigué par son approche de la gestion de la mémoire, notamment en ce qui concerne les pointeurs. Go gère la mémoire d'une manière à la fois efficace et sûre, mais cela peut être un peu une boîte noire si vous ne regardez pas sous le capot. Je souhaite partager quelques informations sur la façon dont Go gère la mémoire avec des pointeurs, la pile et le tas, ainsi que des concepts tels que l'analyse d'échappement et le garbage collection. En cours de route, nous examinerons des exemples de code qui illustrent ces idées dans la pratique.
Avant de plonger dans les pointeurs dans Go, il est utile de comprendre comment fonctionnent la pile et le tas. Ce sont deux zones de mémoire où peuvent être stockées des variables, chacune avec ses propres caractéristiques.
Dans Go, le compilateur décide d'allouer les variables sur la pile ou sur le tas en fonction de la façon dont elles sont utilisées. Ce processus de prise de décision est appelé analyse d'évasion, que nous explorerons plus en détail plus tard.
Dans Go, lorsque vous transmettez des variables comme un entier, une chaîne ou un booléen à une fonction, elles sont naturellement transmises par valeur. Cela signifie qu'une copie de la variable est effectuée et que la fonction fonctionne avec cette copie. Cela signifie que toute modification apportée à la variable à l'intérieur de la fonction n'affectera pas la variable en dehors de sa portée.
Voici un exemple simple :
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) }
Sortie :
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Dans ce code :
À retenir : Le passage par valeur est sûr et simple, mais pour les grandes structures de données, la copie peut devenir inefficace.
Pour modifier la variable d'origine à l'intérieur d'une fonction, vous pouvez lui passer un pointeur. Un pointeur contient l'adresse mémoire d'une variable, permettant aux fonctions d'accéder et de modifier les données d'origine.
Voici comment utiliser les pointeurs :
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) }
Sortie :
Before incrementPointer(): n = 42, address = 0xc00009a040 Inside incrementPointer(): num = 43, address = 0xc00009a040 After incrementPointer(): n = 43, address = 0xc00009a040
Dans cet exemple :
À retenir : L'utilisation de pointeurs permet aux fonctions de modifier la variable d'origine, mais elle introduit des considérations sur l'allocation de mémoire.
Lorsque vous créez un pointeur vers une variable, Go doit s'assurer que la variable dure aussi longtemps que le pointeur. Cela signifie souvent allouer la variable sur le tas plutôt que sur la pile.
Considérez cette fonction :
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) }
Ici, num est une variable locale dans createPointer(). Si num était stocké sur la pile, il serait nettoyé une fois la fonction renvoyée, laissant un pointeur pendant. Pour éviter cela, Go alloue num sur le tas afin qu'il reste valide après la sortie de createPointer().
Pointeurs pendants
Un pointeur suspendu se produit lorsqu'un pointeur fait référence à une mémoire qui a déjà été libérée.
Go évite les pointeurs suspendus avec son garbage collector, garantissant que la mémoire n'est pas libérée alors qu'elle est encore référencée. Cependant, conserver les pointeurs plus longtemps que nécessaire peut entraîner une utilisation accrue de la mémoire ou des fuites de mémoire dans certains scénarios.
L'analyse d'évasion détermine si les variables doivent vivre au-delà de la portée de leur fonction. Si une variable est renvoyée, stockée dans un pointeur ou capturée par une goroutine, elle s'échappe et est allouée sur le tas. Cependant, même si une variable ne s'échappe pas, le compilateur peut l'allouer sur le tas pour d'autres raisons, telles que des décisions d'optimisation ou des limitations de taille de pile.
Exemple d'échappement de variable :
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Dans ce code :
Comprendre l'analyse d'échappement avec go build -gcflags '-m'
Vous pouvez voir ce que décide le compilateur de Go en utilisant l'option -gcflags '-m' :
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) }
Cela affichera des messages indiquant si les variables s'échappent vers le tas.
Go utilise un garbage collector pour gérer l'allocation et la désallocation de mémoire sur le tas. Il libère automatiquement la mémoire qui n'est plus référencée, aidant ainsi à prévenir les fuites de mémoire.
Exemple :
Before incrementPointer(): n = 42, address = 0xc00009a040 Inside incrementPointer(): num = 43, address = 0xc00009a040 After incrementPointer(): n = 43, address = 0xc00009a040
Dans ce code :
À retenir : Le garbage collector de Go simplifie la gestion de la mémoire mais peut introduire une surcharge.
Bien que les pointeurs soient puissants, ils peuvent entraîner des problèmes s'ils ne sont pas utilisés avec précaution.
Bien que le garbage collector de Go aide à éviter les pointeurs suspendus, vous pouvez toujours rencontrer des problèmes si vous conservez les pointeurs plus longtemps que nécessaire.
Exemple :
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) }
Dans ce code :
Voici un exemple où les pointeurs sont directement impliqués :
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Pourquoi ce code échoue :
Réparer la course aux données :
Nous pouvons résoudre ce problème en ajoutant une synchronisation avec un mutex :
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) }
Comment fonctionne ce correctif :
Il convient de noter que La spécification du langage Go ne dicte pas directement si les variables sont allouées sur la pile ou sur le tas. Il s'agit de détails d'implémentation du runtime et du compilateur, permettant une flexibilité et des optimisations qui peuvent varier selon les versions ou implémentations de Go.
Cela signifie :
Exemple :
Même si vous vous attendez à ce qu'une variable soit allouée sur la pile, le compilateur peut décider de la déplacer vers le tas en fonction de son analyse.
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) }
À retenir : Comme les détails d'allocation de mémoire sont une sorte d'implémentation interne et ne font pas partie de la spécification du langage Go, ces informations ne sont que des directives générales et non des règles fixes qui pourraient changer à une date ultérieure.
Lors du choix entre le passage par valeur ou par pointeur, nous devons prendre en compte la taille des données et les implications en termes de performances.
Passage de grandes structures par valeur :
Before increment(): n = 21, address = 0xc000012070 Inside increment(): num = 22, address = 0xc000012078 After increment(): n = 21, address = 0xc000012070
Passage de grandes structures par pointeur :
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) }
Considérations :
En début de carrière, je me souviens d'une époque où j'optimisais une application Go qui traitait de grands ensembles de données. Au départ, j'ai transmis de grandes structures par valeur, en supposant que cela simplifierait le raisonnement sur le code. Cependant, il m'est arrivé de remarquer une utilisation de la mémoire relativement élevée et des pauses fréquentes dans le ramasse-miettes.
Après avoir profilé l'application à l'aide de l'outil pprof de Go en programmation en binôme avec mon senior, nous avons constaté que la copie de structures volumineuses constituait un goulot d'étranglement. Nous avons refactorisé le code pour transmettre des pointeurs au lieu de valeurs. Cela a réduit l'utilisation de la mémoire et amélioré considérablement les performances.
Mais le changement ne s’est pas fait sans défis. Nous devions nous assurer que notre code était thread-safe puisque plusieurs goroutines accédaient désormais aux données partagées. Nous avons implémenté la synchronisation à l'aide de mutex et examiné attentivement le code pour détecter les conditions de concurrence potentielles.
Leçon apprise : Comprendre très tôt comment Go gère l'allocation de mémoire peut vous aider à écrire du code plus efficace, car il est essentiel d'équilibrer les gains de performances avec la sécurité et la maintenabilité du code.
L'approche de Go en matière de gestion de la mémoire (comme partout ailleurs) établit un équilibre entre performances et simplicité. En supprimant de nombreux détails de bas niveau, il permet aux développeurs de se concentrer sur la création d'applications robustes sans s'enliser dans la gestion manuelle de la mémoire.
Points clés à retenir :
En gardant ces concepts à l'esprit et en utilisant les outils de Go pour profiler et analyser votre code, vous pouvez écrire des applications efficaces et sûres.
J'espère que cette exploration de la gestion de la mémoire de Go avec des pointeurs sera utile. Que vous débutiez avec Go ou que vous cherchiez à approfondir vos connaissances, expérimenter le code et observer le comportement du compilateur et du runtime est un excellent moyen d'apprendre.
N'hésitez pas à partager vos expériences ou toutes vos questions — j'ai toujours envie de discuter, d'apprendre et d'écrire davantage sur Go !
Tu sais ? Les pointeurs peuvent être directement créés pour certains types de données et ne le peuvent pas pour certains. Ce petit tableau les couvre.
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 |
S'il vous plaît laissez-moi savoir dans les commentaires si vous aimez ça ; J'essaierai d'ajouter de tels contenus bonus à mes articles à l'avenir.
Merci d'avoir lu ! Pour plus de contenu, pensez à suivre.
Que le code soit avec vous :)
Mes liens sociaux : LinkedIn | GitHub | ? (anciennement Twitter) | Sous-pile | Dev.to | Hashnode
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!