En esta página

Slices y maps

14 min lectura TextoCap. 3 — Datos y errores

Arrays, slices y maps: las colecciones de Go

Go tiene tres tipos de colecciones fundamentales: arrays (raramente usados directamente), slices (la colección más usada) y maps (tablas hash con tipos estáticos). Entender cómo funcionan internamente es clave para escribir código Go eficiente y libre de bugs.

Arrays: colecciones de tamaño fijo

Un array en Go tiene un tamaño fijo que es parte de su tipo. [3]int y [5]int son tipos completamente distintos:

// Declaración
var a [5]int                    // [0 0 0 0 0]
b := [3]string{"a", "b", "c"}  // [a b c]
c := [...]int{1, 2, 3, 4, 5}  // tamaño inferido: [5]int

// Acceso
fmt.Println(b[0])  // "a"
b[1] = "z"

// Iterar
for i, v := range c {
    fmt.Printf("c[%d] = %d\n", i, v)
}

Los arrays son tipos por valor: asignarlos o pasarlos como parámetros hace una copia completa. Por esta razón, en la práctica casi siempre usas slices.

Slices: la colección dinámica de Go

Un slice es una vista sobre un array subyacente. Tiene tres propiedades:

  • Puntero: al primer elemento del array subyacente
  • Longitud (len): número de elementos accesibles
  • Capacidad (cap): número de elementos desde el puntero hasta el final del array subyacente
// Varias formas de crear un slice
s1 := []int{1, 2, 3, 4, 5}      // literal
s2 := make([]int, 5)              // [0 0 0 0 0], len=5, cap=5
s3 := make([]int, 3, 10)          // [0 0 0], len=3, cap=10
var s4 []int                       // nil slice

fmt.Println(len(s1), cap(s1))  // 5 5
fmt.Println(len(s2), cap(s2))  // 5 5
fmt.Println(len(s3), cap(s3))  // 3 10
fmt.Println(s4 == nil)          // true

`append`: agregar elementos

append es la función fundamental para slices. Si la capacidad es suficiente, agrega al mismo array. Si no, asigna un nuevo array más grande (generalmente duplicando la capacidad) y copia los elementos:

s := []int{1, 2, 3}
s = append(s, 4)          // [1 2 3 4]
s = append(s, 5, 6, 7)   // [1 2 3 4 5 6 7]

// Agregar otro slice
otra := []int{8, 9, 10}
s = append(s, otra...)   // [1 2 3 4 5 6 7 8 9 10]

// Siempre asigna el resultado de append
s = append(s, 11)  // NO: append(s, 11) sin asignar

Slicing: crear sub-slices

s := []int{0, 10, 20, 30, 40, 50}

// s[inicio:fin] — fin es exclusivo
s1 := s[1:4]  // [10 20 30], len=3, cap=5
s2 := s[:3]   // [0 10 20], desde el inicio
s3 := s[3:]   // [30 40 50], hasta el final
s4 := s[:]    // copia de la referencia al mismo array

// Three-index slicing: controlar la capacidad del sub-slice
s5 := s[1:3:4]  // [10 20], len=2, cap=3

Cuidado: los sub-slices comparten memoria

original := []int{1, 2, 3, 4, 5}
sub := original[1:3]  // [2 3], comparte array con original

sub[0] = 99
fmt.Println(original)  // [1 99 3 4 5] — ¡original fue modificado!

// Para evitar esto, usa copy
copia := make([]int, 2)
copy(copia, original[1:3])
copia[0] = 99
fmt.Println(original)  // [1 2 3 4 5] — sin cambio

`copy`: copiar elementos entre slices

src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)

n := copy(dst, src)  // copia min(len(dst), len(src)) elementos
fmt.Println(n, dst)  // 3 [1 2 3]

// copy también funciona con strings a []byte
bs := make([]byte, 5)
n = copy(bs, "hola")
fmt.Println(n, bs)  // 4 [104 111 108 97 0]

Eliminar elementos de un slice

Go no tiene una función delete para slices. El patrón estándar usa append:

s := []int{1, 2, 3, 4, 5}
i := 2  // índice a eliminar

// Eliminar elemento en índice i (sin mantener orden)
s[i] = s[len(s)-1]
s = s[:len(s)-1]
// [1 2 5 4]

// Eliminar elemento en índice i (manteniendo orden)
s = append(s[:i], s[i+1:]...)
// Cuidado: esto comparte memoria con el original si hay otros sub-slices

Patrones comunes con slices

// Stack (pila) con un slice
stack := []int{}
stack = append(stack, 1, 2, 3)  // push
top := stack[len(stack)-1]       // peek
stack = stack[:len(stack)-1]     // pop

// Filtrar elementos (filter idiomático)
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
pares := nums[:0]  // reutilizar el backing array
for _, n := range nums {
    if n%2 == 0 {
        pares = append(pares, n)
    }
}
// o con make para copia independiente
var pares2 []int
for _, n := range nums {
    if n%2 == 0 {
        pares2 = append(pares2, n)
    }
}

Maps: tablas hash tipadas

Un map es una colección no ordenada de pares clave-valor con tipos estáticos para ambos:

// Crear un map
m1 := map[string]int{
    "uno":  1,
    "dos":  2,
    "tres": 3,
}

m2 := make(map[string]int)     // map vacío listo para usar
var m3 map[string]int           // nil map — NO se puede escribir en él

// Operaciones básicas
m2["clave"] = 42               // escribir
valor := m2["clave"]           // leer (zero value si no existe)
delete(m2, "clave")            // eliminar
fmt.Println(len(m2))           // número de pares

El idiom "comma ok"

Cuando lees de un map, siempre deberías usar el idiom "comma ok" para distinguir entre "clave no existe" y "clave existe con zero value":

scores := map[string]int{"Ana": 95, "Luis": 0}

// Sin comma ok — no puedes distinguir
a := scores["Ana"]    // 95
p := scores["Pedro"]  // 0 — ¿zero value o realmente 0?

// Con comma ok — forma correcta
if val, ok := scores["Luis"]; ok {
    fmt.Printf("Luis existe con valor %d\n", val)  // Luis existe con valor 0
}

if _, ok := scores["Pedro"]; !ok {
    fmt.Println("Pedro no está en el map")
}

Iterar sobre un map

m := map[string]int{"a": 1, "b": 2, "c": 3}

// El orden de iteración NO está garantizado
for clave, valor := range m {
    fmt.Printf("%s: %d\n", clave, valor)
}

// Solo claves
for clave := range m {
    fmt.Println(clave)
}

// Iteración ordenada: extraer claves, ordenar, iterar
import "sort"

claves := make([]string, 0, len(m))
for k := range m {
    claves = append(claves, k)
}
sort.Strings(claves)
for _, k := range claves {
    fmt.Printf("%s: %d\n", k, m[k])
}

Maps con valores de tipo struct

type Estudiante struct {
    Nombre string
    Nota   float64
}

alumnos := map[string]Estudiante{
    "A001": {Nombre: "Ana", Nota: 9.5},
    "A002": {Nombre: "Luis", Nota: 8.7},
}

// Para modificar un campo, debes reemplazar todo el struct
estudiante := alumnos["A001"]
estudiante.Nota = 10.0
alumnos["A001"] = estudiante

// Alternativa: usar *Estudiante como valor
alumnosPuntero := map[string]*Estudiante{
    "A001": {Nombre: "Ana", Nota: 9.5},
}
alumnosPuntero["A001"].Nota = 10.0  // modificación directa

Nil vs. vacío: la distinción importante

// Slice nil
var sNil []int
fmt.Println(sNil == nil)   // true
fmt.Println(len(sNil))     // 0 — seguro
sNil = append(sNil, 1)     // seguro — append maneja nil

// Slice vacío
sVacío := []int{}
fmt.Println(sVacío == nil) // false
fmt.Println(len(sVacío))   // 0

// JSON: nil → null, vacío → []
// Map nil
var mNil map[string]int
fmt.Println(mNil == nil)   // true
fmt.Println(len(mNil))     // 0 — seguro
val := mNil["key"]         // 0 — seguro
// mNil["key"] = 1         // ¡PANIC! no se puede escribir en map nil

// Map vacío
mVacío := make(map[string]int)  // o map[string]int{}
mVacío["key"] = 1               // seguro

Con el dominio de slices y maps, en la siguiente lección exploraremos los punteros — cómo Go gestiona la memoria sin la aritmética peligrosa de C.

Slices comparten el array subyacente
Cuando haces un sub-slice (s2 := s[1:3]), s2 y s comparten el mismo array subyacente. Modificar s2 modifica s también. Si necesitas una copia independiente, usa copy(). Este comportamiento es la fuente de muchos bugs sutiles en Go.
nil vs. vacío en slices y maps
Un slice nil (var s []int) y un slice vacío (s := []int{}) se comportan igual con len(), append() y range. La diferencia importa al serializar a JSON: nil se convierte en null, el vacío en []. Para maps, un map nil en lectura es seguro (retorna zero values), pero escribir en un map nil causa pánico.