Arrays en Go: Semántica de Valor y Costo de Memoria

En el sistema de tipos de Go, un array es un tipo de dato con semántica de valor pura, cuya longitud forma parte de su identidad de tipo, lo que implica que cualquier asignación o paso de parámetros genera una copia bit a bit de todos sus elementos.

Este comportamiento distingue fundamentalmente a los arrays de los slices. Mientras que en lenguajes como C o Java los arrays suelen comportarse como punteros o referencias encubiertas, Go mantiene una predictibilidad absoluta sobre la memoria: el tamaño de un array es fijo y se conoce en tiempo de compilación. Esta rigidez permite que el compilador optimice el layout de memoria en el stack en lugar del heap, eliminando la necesidad de indirecciones y reduciendo la presión sobre el Garbage Collector.

Mecánica de la copia y comparabilidad

A diferencia de un slice, que es una estructura pequeña (header) que apunta a un bloque de memoria externa, el array es el bloque de memoria en sí mismo. Cuando se asigna un array a una nueva variable, el runtime copia cada elemento individualmente al nuevo scope. Si el array contiene 1024 enteros, se copiarán 1024 enteros. Este determinismo los hace ideales para estructuras donde la inmutabilidad de la fuente es crítica o donde se requieren buffers de tamaño fijo que no deben escapar al heap.

package main

import "fmt"

func modifyArray(a [4]int) {
	a[0] = 999 // Solo modifica la copia local
}

func modifyPointer(a *[4]int) {
	a[0] = 999 // Modifica el array original mediante de-referencia
}

func main() {
	// La longitud [4] es parte del tipo; [4]int != [5]int
	original := [4]int{10, 20, 30, 40}

	modifyArray(original)
	// original sigue siendo [10 20 30 40]

	copy := original
	copy[1] = 50
	// original[1] sigue siendo 20 -> Independencia total de memoria

	modifyPointer(&original)
	// original ahora es [999 20 30 40]

	// Los arrays son comparables si sus elementos lo son
	a := [2]int{1, 2}
	b := [2]int{1, 2}
	// → a == b es true
}
Go

Lo más contraintuitivo para desarrolladores que vienen de otros ecosistemas es que el paso de un array de gran tamaño por valor puede degradar el performance drásticamente debido al tiempo de CPU consumido en la copia de la memoria, a diferencia de los slices cuyo costo de paso por función es constante e independiente de la cantidad de datos que contienen.

Arrays como claves de mapas y optimización del compilador

Un comportamiento no obvio de los arrays es su capacidad para actuar como claves en un map. Dado que los arrays son comparables (comparable types) si sus elementos base lo son, Go genera un hash basado en el contenido completo del array. Esto resulta extremadamente útil para crear índices compuestos sin necesidad de concatenar strings o crear structs complejas, manteniendo una seguridad de tipo estricta.

Escapado al heap por direccionamiento de memoria

Aunque los arrays tienden a residir en el stack para mejorar la localidad de datos, existe un edge case real: el análisis de escape (escape analysis). Si se toma la dirección de un array local mediante el operador & y ese puntero se devuelve en el retorno de una función o se asigna a una interfaz, el compilador se verá obligado a mover el array completo al heap. Esto anula la ventaja de rendimiento del stack y, si el array es grande, puede generar picos de latencia debido al trabajo adicional del Garbage Collector al rastrear un objeto denso de datos que ya no es efímero.


  • Módulo: Colecciones y Memoria
  • Artículo número: #43

Dejar un comentario

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

Scroll al inicio