Anatomía de un Slice: Pointer, Len y Cap

Un slice no es una colección de datos en sí misma; es una cabecera (header) de tres palabras que actúa como una ventana sobre un array subyacente (backing array). Esta estructura consta de un puntero que apunta al primer elemento accesible de la secuencia, un len (length) que indica cuántos elementos hay actualmente en la vista, y un cap (capacity) que define cuántos elementos puede contener el array subyacente antes de que el slice deba solicitar una nueva asignación de memoria.

Esta arquitectura existe para permitir la eficiencia: en lugar de copiar grandes bloques de memoria al pasar un slice a una función o al realizar un sub-slicing, Go simplemente crea una nueva cabecera con un puntero desplazado. Es la razón por la que los slices son extremadamente rápidos para operar, pero también la razón por la cual es vital entender su diseño. Si creas un sub-slice, estás creando una nueva ventana sobre la misma memoria; por tanto, si modificas el sub-slice, estás modificando el array original. Si pierdes de vista la capacidad (cap), podrías realizar operaciones de append que disparen reasignaciones de memoria costosas o, peor aún, corromper datos de otros slices que comparten el mismo backing array.

package main

import (
	"fmt"
)

func main() {
	// 1. Creación con make: controlamos el tamaño inicial y la capacidad
	// Creamos un slice con len 3 y cap 5. El backing array tiene 5 espacios.
	original := make([]int, 3, 5)
	original[0], original[1], original[2] = 10, 20, 30
	fmt.Printf("Original: %v | Len: %d | Cap: %d\n", original, len(original), cap(original))

	// 2. Sub-slicing: compartimos la misma memoria
	// s2 es una nueva cabecera. Su puntero apunta al índice 1 de 'original'.
	// Su len es 2 (índices 1 y 2 de original).
	// Su cap es 4 (la distancia desde el nuevo puntero hasta el final del array original).
	s2 := original[1:3]
	fmt.Printf("Sub-slice s2: %v | Len: %d | Cap: %d\n", s2, len(s2), cap(s2))

	// 3. El efecto colateral: la mutabilidad compartida
	// Modificar s2 altera directamente la memoria que también usa 'original'.
	s2[0] = 99
	fmt.Printf("Original tras modificar s2: %v\n", original)

	// 4. El comportamiento de append y la gestión de la capacidad
	// Como 'original' tiene cap 5, podemos añadir un elemento sin reasignar memoria.
	original = append(original, 40)
	fmt.Printf("Tras append (dentro de capacidad): %v | Cap: %d\n", original, cap(original))

	// Cuando excedemos la capacidad, Go reserva un nuevo array (generalmente más grande)
	// y copia los datos existentes. El puntero de 'original' ahora apunta a una dirección distinta.
	original = append(original, 50, 60, 70)
	fmt.Printf("Tras append (excediendo capacidad): %v | Cap: %d\n", original, cap(original))
}

Desglose del comportamiento

Cuando ejecutas el código anterior, observa cómo original y s2 están entrelazados. En la línea s2 := original[1:3], no se ha copiado el valor 20 o 30. Lo que el runtime ha hecho es crear una nueva estructura de tres palabras donde el pointer apunta a la dirección de memoria de original[1]. Por eso, al ejecutar s2[0] = 99, el valor en la posición original[1] cambia instantáneamente.

En la sección de append, la clave es la comparación entre len y cap. Mientras len(original) < cap(original), el comando append simplemente escribe en el siguiente índice del backing array actual. Es una operación de tiempo constante $O(1)$. Sin embargo, en el último append, la longitud intentada supera la capacidad actual. En ese momento, el runtime de Go realiza una operación pesada: reserva un nuevo bloque de memoria, copia todos los elementos actuales al nuevo bloque y actualiza el pointer de nuestro slice para que apunte a esta nueva ubicación.

El error frecuente

Un error común que causa fugas de memoria (memory leaks) ocurre al sub-slicear un slice muy grande para obtener solo un elemento.

// Escenario peligroso
func leak() []int {
    // Imaginemos que cargamos un archivo de 1GB en un slice de 1 millón de ints
    huge := make([]int, 1_000_000)
    // Queremos solo el primer elemento para procesarlo
    small := huge[:1] 
    return small
}

Aunque small solo parezca contener un entero, su cabecera mantiene un puntero al array de 1 millón de elementos. Mientras small esté vivo (por ejemplo, guardado en una variable global o en una estructura persistente), el recolector de basura (Garbage Collector) no podrá liberar ese array de 1GB de la memoria, porque el slice small todavía tiene una referencia activa a su backing array. Para evitarlo, si solo necesitas una parte pequeña de un array masivo, usa copy() para mover los datos a un nuevo slice con su propia memoria independiente.

Entender esta tríada de puntero, longitud y capacidad es el primer paso para escribir código que no degrade el rendimiento bajo carga.

47

Dejar un comentario

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

Scroll al inicio