Maison >développement back-end >Golang >Arrays vs Slices in Go : Comprendre le fonctionnement « sous le capot » visuellement

Arrays vs Slices in Go : Comprendre le fonctionnement « sous le capot » visuellement

Patricia Arquette
Patricia Arquetteoriginal
2024-12-21 18:27:15278parcourir

Arrays vs Slices in Go: Understanding the

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 :

  1. Dispositions de la mémoire
  2. Mécanismes de croissance
  3. Sémantique de référence
  4. Implications sur les performances

À 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

Tableaux : le conteneur de taille fixe ?

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.

Arrays vs Slices in Go: Understanding the

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
}

Arrays vs Slices in Go: Understanding the

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
}

Pourquoi le tableau est-il copié par défaut ?

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.

Arrays vs Slices in Go: Understanding the

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
}

Tranches

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.

Tranches sous le capot

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 :

Arrays vs Slices in Go: Understanding the

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 :

  1. Il doit pointer vers la mémoire brute sans informations de type
  2. Il permet à Go d'implémenter des tranches pour n'importe quel type T sans générer de code séparé pour chaque type.

Le mécanisme dynamique de slice

essayons de développer l'intuition pour l'algorithme réel sous le capot.

Arrays vs Slices in Go: Understanding the

Si nous nous basons sur intuition nous pouvons faire deux choses :

  1. 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

  2. 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.

Obtenons une meilleure compréhension du point de vue de l'utilisation

Arrays vs Slices in Go: Understanding the

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 :

  1. Calcule la nouvelle capacité (oldCap < 256, donc double à 10)
  2. Alloue un nouveau tableau de sauvegarde (une nouvelle adresse mémoire, disons 300)
  3. Copie les éléments existants dans un nouveau tableau de support
  4. Ajoute un nouvel élément
  5. Mise à jour l'en-tête de la tranche

Arrays vs Slices in Go: Understanding the

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
}

Pourquoi référencer la sémantique ?

  1. Performance : Copier de grandes structures de données coûte cher
  2. Efficacité de la mémoire : éviter la duplication inutile des données
  3. Activation des vues partagées des données : plusieurs tranches peuvent référencer le même tableau de sauvegarde
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

Modèles d'utilisation et mises en garde pour slice

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)

Arrays vs Slices in Go: Understanding the

Terminons cela avec un guide de choix clair :

? Choisissez des tableaux quand :

  1. Vous connaissez la taille exacte à l'avance
  2. Travailler avec des données petites et fixes (comme les coordonnées, les valeurs RVB)
  3. Les performances sont essentielles et les données tiennent sur la pile
  4. Vous voulez taper la sécurité avec la taille

? Choisissez des tranches quand :

  1. La taille peut changer
  2. Travailler avec des données dynamiques
  3. Besoin de plusieurs vues des mêmes données
  4. Traitement des flux/collections

? 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!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn