"Si un ouvrier veut bien faire son travail, il doit d'abord affûter ses outils." - Confucius, "Les Entretiens de Confucius. Lu Linggong"
Page de garde > La programmation > Comment fonctionnent les tableaux Go et deviennent délicats avec For-Range

Comment fonctionnent les tableaux Go et deviennent délicats avec For-Range

Publié le 2024-08-22
Parcourir:515

Ceci est un extrait du message ; l'article complet est disponible ici : Comment fonctionnent les tableaux Go et deviennent délicats avec For-Range.

Le tableau et la tranche Golang classiques sont assez simples. Les tableaux sont de taille fixe et les tranches sont dynamiques. Mais je dois vous dire que Go peut sembler simple en apparence, mais il se passe beaucoup de choses sous le capot.

Comme toujours, nous commencerons par les bases, puis approfondirons un peu. Ne vous inquiétez pas, les tableaux deviennent assez intéressants lorsque vous les regardez sous différents angles.

Nous couvrirons les tranches dans la partie suivante, je les déposerai ici une fois qu'elles seront prêtes.

Qu'est-ce qu'un tableau ?

Les tableaux dans Go ressemblent beaucoup à ceux des autres langages de programmation. Ils ont une taille fixe et stockent des éléments du même type dans des emplacements mémoire contigus.

Cela signifie que Go peut accéder rapidement à chaque élément puisque leurs adresses sont calculées en fonction de l'adresse de départ du tableau et de l'index de l'élément.

func main() {
    arr := [5]byte{0, 1, 2, 3, 4}
    println("arr", &arr)

    for i := range arr {
        println(i, &arr[i])
    }
}

// Output:
// arr 0x1400005072b
// 0 0x1400005072b
// 1 0x1400005072c
// 2 0x1400005072d
// 3 0x1400005072e
// 4 0x1400005072f

Il y a quelques choses à remarquer ici :

  • L'adresse du tableau arr est la même que l'adresse du premier élément.
  • L'adresse de chaque élément est espacée de 1 octet car notre type d'élément est octet.

How Go Arrays Work and Get Tricky with For-Range

Tableau [5]octet{0, 1, 2, 3, 4} en mémoire

Regardez attentivement l'image.

Notre pile croît vers le bas d'une adresse supérieure à une adresse inférieure, n'est-ce pas ? Cette image montre exactement à quoi ressemble un tableau dans la pile, de arr[4] à arr[0].

Alors, cela signifie-t-il que nous pouvons accéder à n'importe quel élément d'un tableau en connaissant l'adresse du premier élément (ou le tableau) et la taille de l'élément ? Essayons ceci avec un tableau int et un package non sécurisé :

func main() {
    a := [3]int{99, 100, 101}

    p := unsafe.Pointer(&a[0])

    a1 := unsafe.Pointer(uintptr(p)   8)
    a2 := unsafe.Pointer(uintptr(p)   16)

    fmt.Println(*(*int)(p))
    fmt.Println(*(*int)(a1))
    fmt.Println(*(*int)(a2))
}

// Output:
// 99
// 100
// 101

Eh bien, nous obtenons le pointeur vers le premier élément, puis calculons les pointeurs vers les éléments suivants en ajoutant des multiples de la taille d'un int, qui fait 8 octets sur une architecture 64 bits. Ensuite, nous utilisons ces pointeurs pour y accéder et les reconvertir en valeurs int.

How Go Arrays Work and Get Tricky with For-Range

Tableau [3]int{99, 100, 101} en mémoire

L'exemple n'est qu'un jeu avec le package non sécurisé pour accéder directement à la mémoire à des fins éducatives. Ne faites pas cela en production sans en comprendre les conséquences.

Maintenant, un tableau de type T n'est pas un type en soi, mais un tableau avec une taille et un type T spécifiques est considéré comme un type. Voici ce que je veux dire :

func main() {
    a := [5]byte{}
    b := [4]byte{}

    fmt.Printf("%T\n", a) // [5]uint8
    fmt.Printf("%T\n", b) // [4]uint8

    // cannot use b (variable of type [4]byte) as [5]byte value in assignment
    a = b 
}

Même si a et b sont des tableaux d'octets, le compilateur Go les considère comme des types complètement différents, le format %T clarifie ce point.

Voici comment le compilateur Go le voit en interne (src/cmd/compile/internal/types2/array.go) :

// An Array represents an array type.
type Array struct {
    len  int64
    elem Type
}

// NewArray returns a new array type for the given element type and length.
// A negative length indicates an unknown length.
func NewArray(elem Type, len int64) *Array { return &Array{len: len, elem: elem} }

La longueur du tableau est "codée" dans le type lui-même, donc le compilateur connaît la longueur du tableau à partir de son type. Essayer d'attribuer un tableau d'une taille à une autre, ou de les comparer, entraînera une erreur de type incompatible.

Littéraux de tableau

Il existe de nombreuses façons d'initialiser un tableau dans Go, et certaines d'entre elles peuvent être rarement utilisées dans des projets réels :

var arr1 [10]int // [0 0 0 0 0 0 0 0 0 0]

// With value, infer-length
arr2 := [...]int{1, 2, 3, 4, 5} // [1 2 3 4 5]

// With index, infer-length
arr3 := [...]int{11: 3} // [0 0 0 0 0 0 0 0 0 0 0 3]

// Combined index and value
arr4 := [5]int{1, 4: 5} // [1 0 0 0 5]
arr5 := [5]int{2: 3, 4, 4: 5} // [0 0 3 4 5]

Ce que nous faisons ci-dessus (sauf pour le premier) consiste à la fois à définir et à initialiser leurs valeurs, ce que l'on appelle un « littéral composite ». Ce terme est également utilisé pour les tranches, les cartes et les structures.

Maintenant, voici une chose intéressante : lorsque nous créons un tableau avec moins de 4 éléments, Go génère des instructions pour mettre les valeurs dans le tableau une par une.

Donc, lorsque nous faisons arr := [3]int{1, 2, 3, 4}, ce qui se passe réellement est :

arr := [4]int{}
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4

Cette stratégie est appelée initialisation du code local. Cela signifie que le code d'initialisation est généré et exécuté dans le cadre d'une fonction spécifique, plutôt que de faire partie du code d'initialisation global ou statique.

Cela deviendra plus clair lorsque vous lirez une autre stratégie d'initialisation ci-dessous, où les valeurs ne sont pas placées dans le tableau une par une comme ça.

"Qu'en est-il des tableaux contenant plus de 4 éléments ?"

Le compilateur crée une représentation statique du tableau dans le binaire, connue sous le nom de stratégie « d'initialisation statique ».

Cela signifie que les valeurs des éléments du tableau sont stockées dans une section en lecture seule du binaire. Ces données statiques sont créées au moment de la compilation, les valeurs sont donc directement intégrées dans le binaire. Si vous êtes curieux de savoir à quoi ressemble [5]int{1,2,3,4,5} dans l'assembly Go :

main..stmp_1 SRODATA static size=40
    0x0000 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00  ................
    0x0010 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00  ................
    0x0020 05 00 00 00 00 00 00 00                          ........

Ce n'est pas facile de voir la valeur du tableau, nous pouvons quand même en tirer quelques informations clés.

Nos données sont stockées dans stmp_1, qui sont des données statiques en lecture seule d'une taille de 40 octets (8 octets pour chaque élément), et l'adresse de ces données est codée en dur dans le binaire.

Le compilateur génère du code pour référencer ces données statiques. Lorsque notre application s'exécute, elle peut utiliser directement ces données pré-initialisées sans avoir besoin de code supplémentaire pour configurer le tableau.

const readonly = [5]int{1, 2, 3, 4, 5}

arr := readonly

"Que diriez-vous d'un tableau avec 5 éléments mais seulement 3 d'entre eux initialisés ?"

Bonne question, ce littéral [5]int{1,2,3} appartient à la première catégorie, où Go met les valeurs une par une dans le tableau.

En parlant de définition et d'initialisation des tableaux, nous devons mentionner que tous les tableaux ne sont pas alloués sur la pile. S'il est trop gros, il est déplacé vers le tas.

Mais quelle est la taille de « trop grand », pourriez-vous vous demander ?

À partir de Go 1.23, si la taille de la variable, et pas seulement du tableau, dépasse une valeur constante MaxStackVarSize, qui est actuellement de 10 Mo, elle sera considérée comme trop grande pour l'allocation de pile et s'échappera vers le tas.

func main() {
    a := [10 * 1024 * 1024]byte{}
    println(&a)

    b := [10*1024*1024   1]byte{}
    println(&b)
}

Dans ce scénario, b se déplacera vers le tas alors que a ne le fera pas.

Opérations sur les tableaux

La longueur du tableau est codée dans le type lui-même. Même si les tableaux n'ont pas de propriété cap, nous pouvons toujours l'obtenir :

func main() {
    a := [5]int{1, 2, 3}
    println(len(a)) // 5
    println(cap(a)) // 5
}

La capacité est sans aucun doute égale à la longueur, mais le plus important est que nous le sachions au moment de la compilation, n'est-ce pas ?

Donc len(a) n'a pas de sens pour le compilateur car ce n'est pas une propriété d'exécution, le compilateur Go connaît la valeur au moment de la compilation.

...

Ceci est un extrait du message ; l'article complet est disponible ici : Comment fonctionnent les tableaux Go et deviennent délicats avec For-Range.

Déclaration de sortie Cet article est reproduit sur : https://dev.to/func25/how-go-arrays-work-and-get-tricky-with-for-range-3i9i?1 En cas d'infraction, veuillez contacter study_golang@163. .com pour le supprimer
Dernier tutoriel Plus>

Clause de non-responsabilité: Toutes les ressources fournies proviennent en partie d'Internet. En cas de violation de vos droits d'auteur ou d'autres droits et intérêts, veuillez expliquer les raisons détaillées et fournir une preuve du droit d'auteur ou des droits et intérêts, puis l'envoyer à l'adresse e-mail : [email protected]. Nous nous en occuperons pour vous dans les plus brefs délais.

Copyright© 2022 湘ICP备2022001581号-3