Maison >développement back-end >Golang >Comment fonctionnent les tableaux Go et deviennent délicats avec For-Range
Ceci est un extrait du post ; 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 surface, mais il se passe beaucoup de choses sous le capot.
Comme toujours, nous commencerons par les bases, puis creuserons un peu plus. 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.
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 :
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.
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 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.
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, quand 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 avec plus de 4 éléments ?"
Le compilateur crée une représentation statique du tableau dans le binaire, appelée stratégie « d'initialisation statique ».
This means the values of the array elements are stored in a read-only section of the binary. This static data is created at compile time, so the values are directly embedded into the binary. If you're curious how [5]int{1,2,3,4,5} looks like in Go assembly:
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 ........
It's not easy to see the value of the array, we can still get some key info from this.
Our data is stored in stmp_1, which is read-only static data with a size of 40 bytes (8 bytes for each element), and the address of this data is hardcoded in the binary.
The compiler generates code to reference this static data. When our application runs, it can directly use this pre-initialized data without needing additional code to set up the array.
const readonly = [5]int{1, 2, 3, 4, 5} arr := readonly
"What about an array with 5 elements but only 3 of them initialized?"
Good question, this literal [5]int{1,2,3} falls into the first category, where Go puts the value into the array one by one.
While talking about defining and initializing arrays, we should mention that not every array is allocated on the stack. If it's too big, it gets moved to the heap.
But how big is "too big," you might ask.
As of Go 1.23, if the size of the variable, not just array, exceeds a constant value MaxStackVarSize, which is currently 10 MB, it will be considered too large for stack allocation and will escape to the heap.
func main() { a := [10 * 1024 * 1024]byte{} println(&a) b := [10*1024*1024 + 1]byte{} println(&b) }
In this scenario, b will move to the heap while a won't.
The length of the array is encoded in the type itself. Even though arrays don't have a cap property, we can still get it:
func main() { a := [5]int{1, 2, 3} println(len(a)) // 5 println(cap(a)) // 5 }
The capacity equals the length, no doubt, but the most important thing is that we know this at compile time, right?
So len(a) doesn't make sense to the compiler because it's not a runtime property, Go compiler knows the value at compile time.
...
This is an excerpt of the post; the full post is available here: How Go Arrays Work and Get Tricky with For-Range.
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!