Maison >développement back-end >Golang >Arrays vs Slices in Go : Comprendre le fonctionnement « sous le capot » visuellement
Avez-vous déjà essayé de préparer vos valises pour un voyage sans savoir combien de temps vous y resterez ? C'est précisément ce qui se passe lorsque nous stockons des données dans Go. Parfois, comme lorsque nous préparons nos valises pour un week-end, nous savons exactement combien de choses nous devons stocker ; d'autres fois, comme lorsque nous préparons un voyage où nous disons : « Je reviendrai quand je serai prêt », nous ne le faisons pas.
Plongeons en profondeur dans le monde des baies Go et découpons les composants internes à travers des illustrations simples. Nous examinerons :
À la fin de cette lecture, vous serez en mesure de comprendre quand utiliser des tableaux et quand utiliser des tranches à l'aide d'exemples réels et de diagrammes de mémoire
Considérez un tableau comme un seul bloc de mémoire où chaque élément est placé les uns à côté des autres, comme une rangée de boîtes parfaitement disposées.
Lorsque vous déclarez var number [5]int, Go réserve exactement suffisamment de mémoire contiguë pour contenir 5 entiers, ni plus, ni moins.
Comme ils ont une mémoire fixe contiguë, elle ne peut pas être dimensionnée pendant l'exécution.
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
La taille fait partie du type du tableau. Cela signifie que [5]int et [6]int sont des types complètement différents, tout comme int et string sont différents.
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
Lorsque vous attribuez ou transmettez des tableaux dans Go, ils créent des copies par défaut. Cela garantit l’isolement des données et évite les mutations inattendues.
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
Très bien, donc vous ne pouvez pas faire var Dynamic [size]int pour définir la taille dynamique, c'est là que slice entre en jeu.
La magie réside dans la façon dont il maintient cette flexibilité tout en maintenant les opérations rapides.
Chaque tranche de Go se compose de trois composants essentiels :
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
Qu'est-ce qui est dangereux.Pointeur ??
Unsafe.Pointer est la façon dont Go gère les adresses mémoire brutes sans contraintes de sécurité de type. C'est "dangereux" car il contourne le système de types de Go, permettant une manipulation directe de la mémoire.
Pensez-y comme l'équivalent de Go au pointeur vide de C.
Quel est ce tableau ?
Lorsque vous créez une tranche, Go alloue un bloc de mémoire contigu dans le tas (contrairement aux tableaux) appelé tableau de sauvegarde. Maintenant, le tableau dans la structure slice pointe vers le début de ce bloc de mémoire.
Le champ du tableau utilise unsafe.Pointer car :
essayons de développer l'intuition pour l'algorithme réel sous le capot.
Si nous nous basons sur intuition nous pouvons faire deux choses :
Nous pourrions réserver un espace aussi grand et l'utiliser selon nos besoins
avantages : Gère les besoins croissants jusqu'à un certain point
inconvénients : gaspillage de mémoire, pourrait pratiquement atteindre la limite
Nous pourrions définir une taille aléatoire au départ et au fur et à mesure que les éléments sont ajoutés, nous pouvons réallouer la mémoire à chaque ajout
avantages : Gère le cas précédent, peut évoluer selon les besoins
inconvénients : la réallocation coûte cher et à chaque ajout, ça va empirer
Nous ne pouvons pas éviter la réaffectation, car lorsque la capacité atteint, il faut croître. Nous pouvons minimiser la réallocation afin que le coût des insertions/ajouts ultérieurs soit constant (O(1)). C’est ce qu’on appelle le coût amorti.
Comment peut-on s'y prendre ?
jusqu'à la version Go v1.17 la formule suivante était utilisée :
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
à partir de la version Go v1.18 :
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
puisque doubler une grande tranche est un gaspillage de mémoire, donc à mesure que la taille de la tranche augmente, le facteur de croissance diminue.
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
ajoutons quelques éléments à notre tranche
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
Puisque nous avons une capacité (5) > longueur (3), Allez :
Utilise un tableau de support existant
Places 10 à l'indice 3
Augmente la longueur de 1
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
Atteignons la limite
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
Oups ! Maintenant que nous avons atteint notre capacité, nous devons grandir. Voici ce qui se passe :
func main() { // Zero-value initialization var nums [3]int // Creates [0,0,0] // Fixed size nums[4] = 1 // Runtime panic: index out of range // Sized during compilation size := 5 var dynamic [size]int // Won't compile: non-constant array bound }
que se passe-t-il si c'est une grosse tranche ?
func main() { // Different types! var a [5]int var b [6]int // This won't compile a = b // compile error: cannot use b (type [6]int) as type [5]int // But this works var c [5]int a = c // Same types, allowed }
La capacité étant de 256, Go utilise la formule de croissance post-1,18 :
Nouvelle capacité = oldCap oldCap/4
256 256/4 = 256 64 = 320
func modifyArrayCopy(arr [5]int) { arr[0] = 999 // Modifies the copy, not original } func modifyArray(arr *[5]int){ arr[0] = 999 // Modifies the original, since reference is passed } func main() { numbers := [5]int{1, 2, 3, 4, 5} modifyArrayCopy(numbers) fmt.Println(numbers[0]) // prints 1, not 999 modifyArray(&numbers) fmt.Println(numbers[0]) // prints 999 }
type slice struct { array unsafe.Pointer // Points to the actual data len int // Current number of elements cap int // Total available space }
voici à quoi ressembleront les en-têtes de tranche :
// Old growth pattern capacity = oldCapacity * 2 // Simple doubling
Mises à jour accidentelles
puisque slice utilise une sémantique de référence, il ne crée pas de copies qui pourraient conduire à une mutation accidentelle vers la tranche d'origine si l'on n'y prend pas garde.
// New growth pattern if capacity < 256 { capacity = capacity * 2 } else { capacity = capacity + capacity/4 // 25% growth }
Opération d'ajout coûteuse
numbers := make([]int, 3, 5) // length=3 capacity // Memory Layout after creation: Slice Header: { array: 0xc0000b2000 // Example memory address len: 3 cap: 5 } Backing Array at 0xc0000b2000: [0|0|0|unused|unused]
Copier vs Ajouter
numbers = append(numbers, 10)
Terminons cela avec un guide de choix clair :
? Choisissez des tableaux quand :
? Choisissez des tranches quand :
? Découvrez le projet notion-to-md ! C'est un outil qui convertit les pages Notion en Markdown, parfait pour les créateurs et développeurs de contenu. Rejoignez notre communauté Discord.
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!