Este es un extracto de la publicación; la publicación completa está disponible aquí: Cómo funcionan los arreglos Go y cómo se complican con For-Range.
La matriz y el corte clásicos de Golang son bastante sencillos. Las matrices tienen un tamaño fijo y los sectores son dinámicos. Pero tengo que decirte que Go puede parecer simple en la superficie, pero tiene mucho que hacer bajo el capó.
Como siempre, comenzaremos con lo básico y luego profundizaremos un poco más. No te preocupes, los arreglos se vuelven bastante interesantes cuando los miras desde diferentes ángulos.
Cubriremos los cortes en la siguiente parte, lo dejaré aquí una vez que esté listo.
Las matrices en Go son muy parecidas a las de otros lenguajes de programación. Tienen un tamaño fijo y almacenan elementos del mismo tipo en ubicaciones de memoria contiguas.
Esto significa que Go puede acceder a cada elemento rápidamente ya que sus direcciones se calculan en función de la dirección inicial de la matriz y el índice del elemento.
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
Hay un par de cosas a tener en cuenta aquí:
Mira la imagen con atención.
Nuestra pila está creciendo hacia abajo desde una dirección superior a una inferior, ¿verdad? Esta imagen muestra exactamente cómo se ve una matriz en la pila, desde arr[4] hasta arr[0].
Entonces, ¿eso significa que podemos acceder a cualquier elemento de una matriz conociendo la dirección del primer elemento (o la matriz) y el tamaño del elemento? Probemos esto con una matriz int y un paquete inseguro:
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
Bueno, obtenemos el puntero al primer elemento y luego calculamos los punteros a los siguientes elementos sumando múltiplos del tamaño de un int, que es de 8 bytes en una arquitectura de 64 bits. Luego usamos estos punteros para acceder y convertirlos nuevamente a los valores int.
El ejemplo es solo un juego con el paquete inseguro para acceder a la memoria directamente con fines educativos. No hagas esto en producción sin comprender las consecuencias.
Ahora, un arreglo de tipo T no es un tipo en sí mismo, pero un arreglo con un tamaño y tipo específico T, se considera un tipo. Esto es lo que quiero decir:
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 }
Aunque tanto a como b son matrices de bytes, el compilador de Go los ve como tipos completamente diferentes, el formato %T aclara este punto.
Así es como el compilador Go lo ve internamente (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 longitud de la matriz está "codificada" en el tipo mismo, por lo que el compilador conoce la longitud de la matriz a partir de su tipo. Intentar asignar una matriz de un tamaño a otro, o compararlos, dará como resultado un error de tipo no coincidente.
Hay muchas formas de inicializar una matriz en Go, y algunas de ellas rara vez se usan en proyectos reales:
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]
Lo que estamos haciendo arriba (excepto el primero) es definir e inicializar sus valores, lo que se denomina "literal compuesto". Este término también se utiliza para sectores, mapas y estructuras.
Ahora, aquí hay algo interesante: cuando creamos una matriz con menos de 4 elementos, Go genera instrucciones para colocar los valores en la matriz uno por uno.
Entonces, cuando hacemos arr := [3]int{1, 2, 3, 4}, lo que realmente sucede es:
arr := [4]int{} arr[0] = 1 arr[1] = 2 arr[2] = 3 arr[3] = 4
Esta estrategia se llama inicialización de código local. Esto significa que el código de inicialización se genera y ejecuta dentro del alcance de una función específica, en lugar de ser parte del código de inicialización global o estático.
Quedará más claro cuando leas otra estrategia de inicialización a continuación, donde los valores no se colocan en la matriz uno por uno de esa manera.
"¿Qué pasa con las matrices con más de 4 elementos?"
El compilador crea una representación estática de la matriz en el binario, lo que se conoce como estrategia de 'inicialización estática'.
Esto significa que los valores de los elementos de la matriz se almacenan en una sección de solo lectura del binario. Estos datos estáticos se crean en el momento de la compilación, por lo que los valores se incrustan directamente en el binario. Si tiene curiosidad sobre cómo se ve [5]int{1,2,3,4,5} en el ensamblado 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 ........
No es fácil ver el valor de la matriz, aún podemos obtener información clave de esto.
Nuestros datos se almacenan en stmp_1, que son datos estáticos de solo lectura con un tamaño de 40 bytes (8 bytes para cada elemento), y la dirección de estos datos está codificada en binario.
El compilador genera código para hacer referencia a estos datos estáticos. Cuando nuestra aplicación se ejecuta, puede usar directamente estos datos preinicializados sin necesidad de código adicional para configurar la matriz.
const readonly = [5]int{1, 2, 3, 4, 5} arr := readonly
"¿Qué pasa con una matriz con 5 elementos pero solo 3 de ellos inicializados?"
Buena pregunta, este literal [5]int{1,2,3} cae en la primera categoría, donde Go coloca el valor en la matriz uno por uno.
Mientras hablamos de definir e inicializar matrices, debemos mencionar que no todas las matrices están asignadas en la pila. Si es demasiado grande, se mueve al montón.
Pero te preguntarás qué tan grande es "demasiado grande".
A partir de Go 1.23, si el tamaño de la variable, no solo de la matriz, excede un valor constante MaxStackVarSize, que actualmente es de 10 MB, se considerará demasiado grande para la asignación de la pila y escapará al montón.
func main() { a := [10 * 1024 * 1024]byte{} println(&a) b := [10*1024*1024 1]byte{} println(&b) }
En este escenario, b se moverá al montón mientras que a no.
La longitud de la matriz está codificada en el tipo mismo. Aunque las matrices no tienen una propiedad de límite, aún podemos obtenerla:
func main() { a := [5]int{1, 2, 3} println(len(a)) // 5 println(cap(a)) // 5 }
La capacidad es igual a la longitud, sin duda, pero lo más importante es que lo sabemos en el momento de la compilación, ¿verdad?
Entonces len(a) no tiene sentido para el compilador porque no es una propiedad de tiempo de ejecución, el compilador Go conoce el valor en tiempo de compilación.
...
Este es un extracto de la publicación; la publicación completa está disponible aquí: Cómo funcionan los arreglos Go y cómo se complican con For-Range.
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3