Arrays en Go: Especificación Técnica y Semántica de Valor

Los arrays en Go son tipos compuestos que representan una secuencia numerada de elementos de un único tipo, cuya longitud está integrada en la definición del tipo y se determina estrictamente en tiempo de compilación. A diferencia de otras estructuras de datos dinámicas, el tamaño de un array no es un atributo de una instancia, sino una propiedad fundamental de su sistema de tipos.

Este diseño proporciona una predecibilidad absoluta en la gestión de memoria. Mientras que en lenguajes como C los arrays suelen decaer en punteros al ser pasados a funciones, o en Java son tratados como referencias en el heap, Go los define como valores atómicos. Este comportamiento resuelve el problema de la indirección innecesaria y permite que el compilador realice optimizaciones de alineación de memoria y bounds-checking con un coste mínimo, garantizando que una estructura de datos de tamaño fijo ocupe un bloque contiguo y exacto en el stack o el heap según determine el escape analysis.

Técnicamente, un array se define por su longitud y su element type. Esta combinación genera un tipo único; por ejemplo, [5]int y [10]int poseen distintos underlying types. No existe la assignability entre ellos, lo que significa que el sistema de tipos de Go impide asignar un array de una longitud a una variable declarada con otra, incluso si comparten el tipo de sus elementos. Esta rigidez asegura que las funciones que aceptan arrays tengan garantías contractuales sobre la cantidad de datos que procesarán.

La característica más relevante para el rendimiento es la semántica de valor. En Go, los arrays no son referencias. Cuando se asigna un array a una nueva variable o se pasa como argumento a una función, el runtime realiza una copia completa de todos sus elementos. Si bien esto garantiza el aislamiento y evita efectos secundarios no deseados (mutaciones compartidas), puede representar un cuello de botella si se manipulan arrays de gran envergadura sin el uso de punteros. Por último, la operador de igualdad (==) está definido para los arrays siempre que el tipo de sus elementos sea comparable. Dos arrays son iguales si tienen el mismo tipo y todos sus elementos correspondientes son iguales.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// Declaración con tamaño fijo y literal de array
	// El tamaño es parte del tipo: [3]int != [4]int
	a := [3]int{10, 20, 30}
	
	// Uso del elipsis para dejar que el compilador cuente los elementos
	b := [...]int{10, 20, 30}

	// Comparación directa mediante el operador de igualdad
	fmt.Printf("¿a es igual a b?: %v\n", a == b) // → true

	// Demostración de semántica de valor (copia completa)
	c := a
	c[0] = 99
	fmt.Printf("Original a[0]: %d, Copia c[0]: %d\n", a[0], c[0]) 
	// → Original a[0]: 10, Copia c[0]: 99

	// Tamaño en memoria: 3 elementos * 8 bytes (en x64)
	fmt.Printf("Bytes ocupados por a: %d\n", unsafe.Sizeof(a)) // → 24

	// Una función que recibe [3]int solo acepta arrays de esa longitud exacta
	procesar(a)
}

func procesar(arr [3]int) {
	// arr es una copia local, los cambios aquí no afectan al llamador
	arr[1] = 500
}
Go

La copia íntegra de la estructura al pasarla por valor es el aspecto más contraintuitivo para desarrolladores acostumbrados a la semántica de referencia, ya que una simple llamada a función puede disparar una copia masiva en el stack si el array contiene miles de elementos.

Incompatibilidad por longitud y la limitación de comparabilidad

Un comportamiento interno crítico del compilador de Go es que la longitud del array debe ser una expresión constante no negativa representable por un valor de tipo int. Esto implica que no se pueden crear arrays cuya longitud se determine por una variable en tiempo de ejecución; para ese caso de uso, el lenguaje provee slices. Esta limitación es la que permite que el tamaño del array sea conocido por el sistema de tipos durante el análisis estático.

Un edge case relevante ocurre cuando se intentan comparar arrays cuyos elementos no soportan el operador de igualdad. Aunque la estructura [N]T es técnicamente comparable, si el tipo T es un tipo no comparable (como un slice, un map o una function), el array resultante hereda esa limitación. Intentar comparar [2][]string con == provocará un error de compilación inmediato (invalid operation: a == b (slice can only be compared to nil)). Esto refuerza la filosofía de Go de que la comparabilidad de un tipo compuesto es recursiva y depende estrictamente de la comparabilidad de sus constituyentes.


  • Módulo: Sistema de Tipos
  • Artículo número: #24

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio