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) aappendcon 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 quemake+copyporque el compilador optimiza esta forma de “one-liner”. - Filtrado in-place: Usamos la variable
ncomo un índice de escritura. A medida que iteramos, solo escribimos entasks[n]si el elemento cumple el criterio. Esto transforma el slice original sobre su propio array subyacente, manteniendo la localidad de la caché y evitandomalloc. - 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 truncamientotasks[: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 conappend(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 ens[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]
N° 50