Anatomía de un Slice en Go: Puntero, Longitud y Capacidad

Un slice en Go es un descriptor de segmento para un array subyacente, actuando como una vista ligera y dinámica sobre una secuencia contigua de elementos. A diferencia de los arrays, cuyo tamaño es parte inherente de su tipo y se fija en tiempo de compilación, los slices proporcionan una abstracción flexible que permite manipular subconjuntos de datos sin incurrir en el costo de copias masivas de memoria.

Este comportamiento resuelve la rigidez de la gestión de colecciones en lenguajes de bajo nivel y optimiza el rendimiento en comparación con lenguajes que dependen de la copia por valor para estructuras dinámicas. En Go, un slice no almacena los datos directamente; en su lugar, el slice header (o descriptor) mantiene la información necesaria para acceder y gestionar un array subyacente (backing array). Al separar la estructura de control de los datos reales, Go permite que múltiples slices apunten al mismo segmento de memoria, facilitando operaciones de rebanado (slicing) extremadamente eficientes.

Internamente, el descriptor de un slice es una estructura pequeña de tres campos de tamaño fijo: un puntero (unsafe.Pointer), una longitud (len) y una capacidad (cap). El puntero identifica el primer elemento del array subyacente accesible a través del slice, que no necesariamente coincide con el inicio del array original. La longitud define el número de elementos que el slice contiene actualmente, mientras que la capacidad representa el número máximo de elementos que el slice puede albergar antes de requerir la asignación de un nuevo array subyacente más grande.

Cuando un slice se pasa como argumento a una función, Go aplica su semántica estándar de paso por valor. Sin embargo, lo que se copia no son los elementos del array, sino únicamente el descriptor de 24 bytes (en arquitecturas de 64 bits). Esta copia del descriptor contiene el mismo puntero a la memoria original. Por lo tanto, si la función modifica un elemento a través del slice, el cambio será visible para el llamador. No obstante, si la función altera la longitud o capacidad del slice local (por ejemplo, mediante append), estas mutaciones en el descriptor no se reflejarán en el slice original del llamador, ya que este posee su propia copia independiente de los campos len y cap.

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	// Inicialización con make: puntero al array, len=3, cap=5
	s := make([]int, 3, 5)
	s[0], s[1], s[2] = 10, 20, 30

	fmt.Printf("Main inicial: len=%d cap=%d ptr=%p\n", len(s), cap(s), s)
	// Main inicial: len=3 cap=5 ptr=0xc00001a120

	modificarElemento(s)
	fmt.Printf("Después de modificar: %v\n", s) 
	// → [99 20 30] (El cambio persiste porque el puntero es el mismo)

	intentarExpandir(s)
	fmt.Printf("Main final: len=%d cap=%d\n", len(s), cap(s))
	// → len=3 cap=5 (La longitud del main no cambió a pesar del append en la función)
}

func modificarElemento(data []int) {
	data[0] = 99 // Acceso directo al array subyacente
}

func intentarExpandir(data []int) {
	// append modifica el campo 'len' en la COPIA del descriptor 'data'
	data = append(data, 40) 
	fmt.Printf("Dentro de la función: len=%d cap=%d\n", len(data), cap(data))
	// → len=4 cap=5
}
Go

La desconexión entre la longitud del llamador y la del receptor tras un append es el origen más frecuente de errores lógicos, lo que obliga a la convención idiomática de devolver siempre el slice modificado o pasar un puntero al slice (*[]int) cuando se requiere alterar su estructura.

Sombreado de memoria y la persistencia del array subyacente

Un comportamiento crítico del runtime es que un array subyacente permanece en memoria mientras exista al menos un slice apuntando a él, independientemente de qué tan pequeña sea la ventana definida por len. Esto ocurre porque el Garbage Collector (GC) rastrea el puntero base del descriptor.

Un edge case real de alto impacto se produce cuando se genera un slice pequeño a partir de un slice o array masivo (por ejemplo, leyendo un archivo de 1GB y extrayendo solo 10 bytes). Aunque el slice resultante tenga una longitud de 10, su descriptor podría mantener un puntero a un array subyacente de 1GB. Si el slice original se pierde pero el pequeño persiste, el gigabyte completo de memoria no podrá ser liberado por el GC. Para mitigar este riesgo en sistemas de alta disponibilidad, se debe utilizar la función copy para trasladar los datos necesarios a un nuevo slice con capacidad ajustada, permitiendo que el array masivo sea recolectado.


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

Dejar un comentario

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

Scroll al inicio