arrays-vs-slices-diferencias-longitud-capacidad

Arrays y slices en Go: diferencias, memoria, len y cap

  • 5 min

Los arrays y los slices en Go son dos formas de almacenar secuencias de valores del mismo tipo, pero con una diferencia enorme: el array tiene tamaño fijo y el slice es una vista flexible sobre un array.

Si vienes de JavaScript o Python, estás acostumbrado a tener una lista dinámica donde metes cosas y ella crece sola. Si vienes de C, conoces los arrays estáticos y los punteros.

En Go tenemos dos tipos de colecciones secuenciales que, aunque se declaran de forma parecida, tienen semánticas distintas: los arrays y los slices.

Entender esa diferencia evita errores al copiar o compartir datos. Vamos a abrir el capó de Go para ver cómo se relacionan con la memoria y por qué normalmente usaremos slices.

Arrays: tamaño fijo y semántica de valor

Un array en Go es una secuencia de elementos de un tamaño fijo y determinado en tiempo de compilación.

La característica definitoria de un array es que su longitud es parte de su tipo.

  • [5]int es un array de 5 enteros.
  • [10]int es un array de 10 enteros.
  • Son tipos distintos. No puedes asignar uno al otro.

Declaración

var numeros [5]int // Array de 5 enteros inicializados a 0
numeros[0] = 10
numeros[4] = 20

// Declaración e inicialización literal
colores := [3]string{"Rojo", "Verde", "Azul"}

// Dejar que el compilador cuente por nosotros (...)
primos := [...]int{2, 3, 5, 7, 11} // Es un array de tamaño 5
Copied!

Comportamiento por valor

A diferencia de C (donde un array es un puntero al primer elemento) o Java (donde es una referencia), en Go los arrays son VALORES.

Si asignas un array a otra variable, o lo pasas como argumento a una función, se hace una copia completa de todos sus datos.

a := [3]int{1, 2, 3}
b := a // SE COPIA todo el array a 'b'

b[0] = 99

fmt.Println(a) // Imprime [1 2 3] - 'a' no ha cambiado
fmt.Println(b) // Imprime [99 2 3]
Copied!

Pasar arrays grandes [1000000]int a funciones tiene semántica de copia y puede ser costoso. Si queremos compartir los datos sin copiarlos, normalmente pasamos un slice o un puntero al array.

Slices: ventanas flexibles

Un slice es una vista construida sobre un array subyacente. Su longitud puede cambiar mediante operaciones como append, y es el tipo que usaremos habitualmente para secuencias.

Sintaxis: Es igual que el array, pero sin número dentro de los corchetes. var miSlice []int

Qué contiene un slice

Como modelo mental, podemos pensar en un slice como un descriptor pequeño con tres datos:

  1. Referencia: apunta al primer elemento accesible dentro de un array subyacente.
  2. Longitud (len): Cuántos elementos contiene el slice.
  3. Capacidad (cap): Cuántos elementos podría contener el slice sin necesidad de reservar más memoria (es decir, el tamaño del array subyacente desde el puntero).

Crear slices

Podemos crear slices de varias formas:

// 1. Literal (Parece un array, pero sin número en [])
nombres := []string{"Ana", "Beto", "Carla"}

// 2. A partir de un array existente (Operación de corte)
arrayBase := [5]int{10, 20, 30, 40, 50}
miSlice := arrayBase[1:4] // Desde índice 1 hasta (4-1) -> {20, 30, 40}

// 3. Usando la función make()
// make([]Tipo, longitud, capacidad)
buffer := make([]int, 5, 10)
Copied!

Copia del encabezado, datos compartidos

Como un slice es solo un “puntero vitaminado”, cuando copias un slice o lo pasas a una función, solo copias el encabezado pequeño. Los dos slices siguen apuntando al mismo array subyacente.

sliceA := []int{1, 2, 3}
sliceB := sliceA // Copiamos el descriptor (referencia, len, cap)

sliceB[0] = 99

fmt.Println(sliceA) // Imprime [99 2 3] -> ¡'sliceA' HA CAMBIADO!
Copied!

Diferencia entre longitud (len) y capacidad (cap)

Esta distinción es fundamental para entender el rendimiento en Go.

  • Longitud (len): Es el número de elementos que el slice “ve” actualmente. Son los elementos a los que puedes acceder (slice[0] hasta slice[len-1]).
  • Capacidad (cap): Es el espacio real disponible en el array que hay detrás del telón.

Podemos imaginar un vaso de 500 ml (cap) que ahora mismo contiene 200 ml (len).

func main() {
    // Creamos slice: longitud 2, capacidad 5
    s := make([]int, 2, 5)
    s[0] = 10
    s[1] = 20

    fmt.Printf("Len: %d, Cap: %d, Valor: %v\n", len(s), cap(s), s)
    // Salida: Len: 2, Cap: 5, Valor: [10 20]

    // fmt.Println(s[3]) // ERROR: panic, index out of range
    // Aunque hay capacidad (memoria reservada), el slice solo "ve" hasta len.
}
Copied!

Extender una vista con reslicing

Como tenemos capacidad de sobra (el vaso no está lleno), podemos extender el slice para “ver” más elementos que ya están en memoria pero estaban ocultos.

// Extendemos el slice una posición más a la derecha
s = s[:3]
fmt.Printf("Len: %d, Cap: %d\n", len(s), cap(s))
// Ahora len es 3. El valor de s[2] será 0 (zero value) porque ya estaba en memoria.
Copied!

Operador de corte [low:high]

El operador de corte nos permite crear un nuevo slice apuntando a un tramo de un array o de otro slice.

Sintaxis: origen[inicio : fin]

  • Incluye el índice inicio.
  • Excluye el índice fin.
arr := [5]int{0, 1, 2, 3, 4}

s1 := arr[1:4] // Indices 1, 2, 3 -> Valor: [1 2 3]
s2 := arr[:2]  // Indices 0, 1    -> Valor: [0 1] (inicio default 0)
s3 := arr[2:]  // Indices 2..fin  -> Valor: [2 3 4] (fin default len)
Copied!

Cuidado con la memoria: Si tienes un array enorme de 1GB en memoria y creas un slice pequeño que apunta a solo 3 elementos de ese array, el array de 1GB entero se mantiene en memoria porque el slice lo está referenciando. El Garbage Collector no puede liberarlo.