Manipulación idiomática de slices: copy, filtrado e inserción

Un slice en Go no es una estructura de datos per se, sino una cabecera (descriptor) que apunta a un array subyacente. Esta cabecera contiene tres campos: un puntero al primer elemento, la longitud (len) y la capacidad (cap). Cuando manipulas un slice, lo que realmente estás alterando son las dimensiones de esa cabecera o los datos en el array subyacente.

La función copy(dst, src) es la herramienta fundamental para mover datos entre arrays subyacentes de forma eficiente, operando internamente de manera similar a un memmove en C. Es vital entender que copy solo traslada la cantidad mínima de elementos necesaria para cubrir el min(len(dst), len(src)).

Existen patrones optimizados para las operaciones más comunes: para clonar un slice sin preocuparte por la capacidad del original, el uso de append([]T(nil), src...) es la forma más limpia y eficiente. Para eliminar elementos, tienes dos estrategias: si el orden de los datos no importa, puedes sobreescribir el elemento con el último de la lista y reducir la longitud en $O(1)$; si el orden es crítico, debes usar copy para desplazar los elementos hacia la izquierda en $O(n)$. Finalmente, para filtrar una colección sin generar basura en el heap (evitando nuevas asignaciones de memoria y reduciendo la presión sobre el Garbage Collector), lo más eficiente es reutilizar el mismo slice mediante un puntero de escritura manual.

Estos trucos son esenciales cuando escribes código de alto rendimiento o sistemas distribuidos donde cada asignación de memoria cuenta. Si se usan incorrectamente, especialmente con slices de punteros, puedes dejar referencias “huérfanas” en el array subyacente, impidiendo que el recolector de basura libere la memoria.

package main

import (
	"fmt"
)

type Task struct {
	ID    int
	Label string
}

func main() {
	// 1. Clonación idiomática
	// Queremos una copia que no comparta el mismo array subyacente que original.
	original := []Task{{1, "Setup"}, {2, "Code"}, {3, "Test"}}
	clon := append([]Task(nil), original...)
	fmt.Printf("Original: %v\nClon:     %v\n", original, clon)

	// 2. Filtrado in-place (Sin nuevas asignaciones)
	// Reutilizamos 'tasks' para evitar que el GC tenga que limpiar el slice anterior.
	tasks := []Task{{1, "A"}, {2, "X"}, {3, "B"}, {4, "X"}, {5, "C"}}
	n := 0
	for _, t := range tasks {
		// Solo mantenemos las tareas que no son "X"
		if t.Label != "X" {
			tasks[n] = t
			n++
		}
	}
	tasks = tasks[:n]
	fmt.Printf("\nFiltrado (solo no-X): %v\n", tasks)

	// 3. Eliminar elemento preservando orden (Eliminar índice 1: "B")
	// Usamos copy para desplazar los elementos a la izquierda.
	copy(tasks[1:], tasks[2:])
	tasks = tasks[:len(tasks)-1]
	fmt.Printf("Eliminado ordenado (índice 1): %v\n", tasks)

	// 4. Insertar elemento en posición i (Insertar "NEW" en índice 1)
	// Objetivo: de [1, A, 3, C] pasar a [1, 99, A, 3, C]
	s := []Task{{1, "A"}, {3, "C"}}
	v := Task{99, "NEW"}
	i := 1

	// Expandimos el slice un elemento más para hacer espacio
	s = append(s, Task{})
	// Desplazamos los elementos desde la posición 'i' hacia la derecha
	copy(s[i+1:], s[i:])
	// Insertamos el nuevo valor
	s[i] = v
	fmt.Printf("Insertado en índice 1: %v\n", s)

	// 5. Eliminar elemento rápido (No preserva orden)
	// Si el orden no importa, intercambiamos con el último y truncamos.
	// De [A, NEW, C] a [A, C]
	s = []Task{{1, "A"}, {2, "B"}, {3, "C"}}
	idxToRemove := 1
	s[idxToRemove] = s[len(s)-1]
	s = s[:len(s)-1]
	fmt.Printf("Eliminado rápido (sin orden): %v\n", s)
}

Concepto Breakdown

  • Clonación (append([]Task(nil), original...)): Al pasar un slice con capacidad cero (nil) a append con un argumento variádico, el runtime detecta que necesita espacio y asigna un nuevo array subyacente con el tamaño exacto necesario, copiando todos los elementos. Es más eficiente que make + copy porque el compilador optimiza esta forma de “one-liner”.
  • Filtrado in-place: Usamos la variable n como un índice de escritura. A medida que iteramos, solo escribimos en tasks[n] si el elemento cumple el criterio. Esto transforma el slice original sobre su propio array subyacente, manteniendo la localidad de la caché y evitando malloc.
  • Eliminación con copy: copy(tasks[1:], tasks[2:]) toma el segmento que empieza en el índice 2 y lo desplaza una posición hacia la izquierda (empezando en el 1). El truncamiento tasks[:len(tasks)-1] es lo que realmente reduce la longitud visible.
  • Inserción con copy: Para insertar sin crear un slice intermedio, primero ampliamos el slice con append(s, Task{}). Esto puede disparar una reasignación si se alcanza la capacidad. Luego, copy(s[i+1:], s[i:]) desplaza el bloque de datos necesario hacia la derecha para abrir el hueco en s[i].
  • Eliminación rápida: En la última operación, s[idxToRemove] = s[len(s)-1] es una operación $O(1)$. Es ideal para colas o sets donde el orden no sea una preocupación, ya que evita el desplazamiento masivo de memoria.

El error frecuente

Cuando trabajas con slices de punteros (o interfaces), simplemente reducir la longitud del slice no es suficiente para liberar la memoria. Si tienes un slice de punteros []*User y eliminas el último elemento simplemente haciendo s = s[:len(s)-1], el array subyacente sigue conteniendo el puntero en la posición que ahora es “invisible”. Ese objeto no será recolectado por el Garbage Collector porque el array subyacente sigue teniendo una referencia hacia él.

Para evitarlo, siempre debes limpiar la posición:

// FORMA INCORRECTA (Memory Leak)
s[i] = s[len(s)-1]
s = s[:len(s)-1]

// FORMA CORRECTA (Limpieza de punteros)
s[i] = s[len(s)-1]
s[len(s)-1] = nil // Evitamos que el GC mantenga el objeto vivo
s = s[:len(s)-1]

50

Dejar un comentario

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

Scroll al inicio