manipulacion-slices-append-copy-delete

Manipulación de slices en Go con append, copy y slicing

  • 5 min

Manipular slices en Go consiste en crear nuevas vistas, ampliar secuencias y copiar datos de forma explícita. Parece más manual que en otros lenguajes, pero también deja muy claro qué pasa en memoria.

En el artículo anterior vimos que los slices son vistas de un array subyacente. Ahora toca ampliarlos, copiarlos y eliminar elementos.

Aunque los arrays tienen tamaño fijo, append puede devolver un slice respaldado por un array nuevo cuando necesita crecer. Vamos a aprender las operaciones esenciales para añadir, copiar y borrar.

La función append: crecimiento dinámico

La función append es la herramienta principal para añadir elementos al final de un slice.

Conceptualmente recibe un slice y uno o varios valores del mismo tipo. Después devuelve el descriptor actualizado del slice.

package main

import "fmt"

func main() {
    var numeros []int // Slice nulo (len=0, cap=0)

    // Añadimos un elemento
    numeros = append(numeros, 10)

    // Añadimos varios a la vez
    numeros = append(numeros, 20, 30, 40)

    fmt.Println(numeros) // [10 20 30 40]
}
Copied!

Es un error muy común escribir append(numeros, 10) sin asignar el resultado. Como append puede cambiar el array subyacente (si necesita reservar más capacidad), siempre debes usar el valor que devuelve: slice = append(slice, valor)

¿Cómo funciona la memoria por debajo?

Cuando haces un append:

  1. Si hay capacidad de sobra en el array subyacente, Go simplemente aumenta la longitud (len) y pone el valor en el hueco libre. Es muy rápido.
  2. Si NO hay capacidad (el array está lleno), Go crea un nuevo array con más capacidad, copia los datos viejos al nuevo, añade el elemento y te devuelve un slice apuntando a ese nuevo array.

Unir dos slices

Si quieres pegar un slice entero al final de otro, usamos el operador de “desempaquetado” ... (ellipsis).

s1 := []int{1, 2, 3}
s2 := []int{4, 5}

// s1 = append(s1, s2) // ERROR: s2 no es un int
s1 = append(s1, s2...) // CORRECTO: Desempaquetamos s2
fmt.Println(s1) // [1 2 3 4 5]
Copied!

La función copy: duplicar datos

Como vimos, sliceA := sliceB no copia los datos, solo el puntero. Si queremos dos copias independientes (para modificar una sin tocar la otra), usamos la función copy.

Sintaxis: copy(destino, origen)

Idea importante: copy solo copia el mínimo de elementos entre la longitud de destino y origen.

func main() {
    origen := []int{1, 2, 3, 4, 5}

    // CUIDADO: Destino debe tener longitud (espacio) reservado
    destino := make([]int, 3) // len=3

    elementosCopiados := copy(destino, origen)

    fmt.Println(destino) // [1 2 3] (Solo cabían 3)
    fmt.Println("Copiados:", elementosCopiados) // 3
}
Copied!

Si hubiéramos hecho destino := make([]int, 0) o var destino []int, la función copy no haría nada porque la longitud del destino sería 0.

Cortar slices

El operador [:] ya nos suena. Vamos a ver algunos patrones habituales para manipular secuencias.

Sea a := []int{0, 1, 2, 3, 4, 5}:

OperaciónSintaxisResultado
Quitar el primeroa = a[1:][1 2 3 4 5]
Quitar el últimoa = a[:len(a)-1][0 1 2 3 4]
Tomar los N primerosa = a[:n][0 ... n-1]

Borrar un elemento

Desde Go 1.21, el paquete estándar slices ofrece slices.Delete. Recibe el slice y un intervalo semiabierto [i:j) y devuelve el slice sin esos elementos:

import "slices"

letras := []string{"A", "B", "C", "D", "E"}
letras = slices.Delete(letras, 2, 3)
fmt.Println(letras) // [A B D E]
Copied!

Además de desplazar los elementos, slices.Delete pone a cero la parte descartada del array subyacente. Así evita retener objetos a través de punteros que ya no son visibles desde el slice.

También conviene conocer el patrón manual, porque explica lo que ocurre por debajo. Para borrar el elemento del índice i, unimos lo que hay antes y después de i:

func main() {
    letras := []string{"A", "B", "C", "D", "E"}
    indiceBorrar := 2 // Queremos borrar la "C"

    // letras[:2] -> ["A", "B"]
    // letras[3:] -> ["D", "E"]

    letras = append(letras[:indiceBorrar], letras[indiceBorrar+1:]...)

    fmt.Println(letras) // ["A", "B", "D", "E"]
}
Copied!

Datos compartidos y referencias retenidas

Este método normalmente reutiliza el array subyacente, pero tiene dos efectos secundarios. Al mover los elementos para tapar el hueco, otros slices que compartan ese array verán los cambios. Además, el último hueco queda fuera de la longitud, pero conserva su valor; si contiene referencias y el array seguirá vivo, conviene ponerlo a cero.

Borrado rápido (sin preservar orden)

Si no te importa el orden de los elementos, hay una forma mucho más rápida (O(1)) de borrar:

  1. Copias el último elemento en la posición que quieres borrar.
  2. Eliminas el último elemento (reduces el slice).
letras := []string{"A", "B", "C", "D", "E"}
i := 2 // Borrar "C"

letras[i] = letras[len(letras)-1] // Copiamos "E" en posición de "C"
letras[len(letras)-1] = ""       // Liberamos la referencia descartada
letras = letras[:len(letras)-1]   // Reducimos la longitud

// Resultado: ["A", "B", "E", "D"] -> Orden cambiado, pero muy rápido.
Copied!