Arrays y slices en Go: la diferencia crucial y cómo funciona append

En Go, los arrays son tipos de valor con tamaño fijo definido en tiempo de compilación. Los slices son vistas dinámicas sobre arrays subyacentes. Entender esta distinción es fundamental porque define cómo se comporta tu código al pasar colecciones a funciones, al usar append y al copiar datos.

Arrays: tamaño fijo y parte del tipo

package main

import "fmt"

func main() {
    var a [3]int          // [0 0 0]
    b := [3]int{1, 2, 3} // literal
    c := [...]int{4, 5, 6, 7} // el compilador cuenta: [4]int

    fmt.Println(a) // [0 0 0]
    fmt.Println(b) // [1 2 3]
    fmt.Println(c) // [4 5 6 7]
    fmt.Println(len(c)) // 4
}

[3]int y [4]int son tipos distintos e incompatibles. No puedes asignar uno al otro ni pasarlos donde se espera el otro.

Slices: longitud dinámica sobre un array

s := []int{10, 20, 30, 40, 50}
fmt.Println(s[1:3])  // [20 30] — desde índice 1 hasta 2 (sin incluir 3)
fmt.Println(s[:2])   // [10 20]
fmt.Println(s[3:])   // [40 50]

fmt.Println(len(s))  // 5 — longitud actual
fmt.Println(cap(s))  // 5 — capacidad del array subyacente

Un slice tiene tres componentes internos: un puntero al primer elemento del array subyacente, la longitud (len) y la capacidad (cap). Dos slices pueden compartir el mismo array subyacente, lo que tiene implicaciones importantes.

append y crecimiento de capacidad

Cuando añades elementos con append y la capacidad es insuficiente, Go reserva un nuevo array más grande, copia los datos y devuelve un nuevo slice:

s := make([]int, 0, 3) // longitud 0, capacidad 3

for i := 0; i < 6; i++ {
    s = append(s, i)
    fmt.Printf("len=%d cap=%d %vn", len(s), cap(s), s)
}
// len=1 cap=3 [0]
// len=2 cap=3 [0 1]
// len=3 cap=3 [0 1 2]
// len=4 cap=6 [0 1 2 3]  ? nueva reserva, capacidad dobla
// len=5 cap=6 [0 1 2 3 4]
// len=6 cap=6 [0 1 2 3 4 5]

Como append puede devolver un nuevo slice, siempre tienes que asignar el resultado de vuelta a la variable.

Slices comparten memoria: el peligro oculto

original := []int{1, 2, 3, 4, 5}
vista := original[1:3] // [2 3], mismo array subyacente

vista[0] = 99
fmt.Println(original) // [1 99 3 4 5] ? el original cambió

Para evitar este acoplamiento, usa copy:

copia := make([]int, len(vista))
copy(copia, vista)
copia[0] = 0
fmt.Println(original) // [1 99 3 4 5] ? sin cambios

copy: copiar entre slices

copy(dst, src) copia el mínimo entre len(dst) y len(src) elementos y devuelve el número copiado:

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Println(n, dst) // 3 [1 2 3]

Pasar un slice a una función

Un slice se pasa por valor, pero el valor incluye el puntero al array subyacente. La función puede modificar los elementos del array original, pero no puede cambiar la longitud o el puntero que ve el caller:

func doblar(s []int) {
    for i := range s {
        s[i] *= 2 // modifica el array subyacente compartido
    }
}

func main() {
    nums := []int{1, 2, 3}
    doblar(nums)
    fmt.Println(nums) // [2 4 6]
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP