Basado en los ejemplos de gobyexample.com. Cubre el bloque de “tipos compuestos” del sitio —Arrays, Slices y Maps— más Range over Built-in Types, que es la forma estándar de recorrerlos y por eso se explica mejor junto a ellos que por separado.

Todos los bloques de código de este documento son programas completos: incluyen package main y sus import, así que se pueden copiar directo a un archivo .go y correr con go run archivo.go.


1. Arrays

Un array en Go es una secuencia numerada de elementos de un tipo fijo, con una longitud fija que forma parte de su tipo. Esto es distinto a la mayoría de los lenguajes dinámicos: [5]int y [6]int son tipos diferentes, no solo tamaños distintos del mismo tipo.

package main

import "fmt"

func main() {
    var a [5]int
    a[4] = 100
    fmt.Println("get:", a[4])
    fmt.Println("len:", len(a))

    b := [5]int{1, 2, 3, 4, 5}
    fmt.Println("dcl:", b)

    var mat [2][3]int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            mat[i][j] = i + j
        }
    }
    fmt.Println("2d:", mat)
}

Puntos clave:

  • len(a) devuelve el número de elementos.
  • El valor cero de un array numérico es todo ceros; para strings, cadenas vacías; etc. No hace falta inicializarlo explícitamente.
  • Los arrays se pueden anidar para crear estructuras multidimensionales ([2][3]int).
  • Los arrays son de valor: al asignar un array a otra variable o pasarlo a una función, Go copia todos sus elementos. Esto los hace poco prácticos como parámetro de función en la mayoría de los casos, y es la razón principal por la que en la práctica casi siempre se usan slices en lugar de arrays.

2. Slices

Un slice es la estructura de datos de secuencia que realmente se usa en el día a día de Go. A diferencia del array, no tiene longitud fija en su tipo y es, internamente, una vista (puntero + longitud + capacidad) sobre un array subyacente.

package main

import "fmt"

func main() {
    s := make([]string, 3)
    s[0] = "a"
    s[1] = "b"
    s[2] = "c"
    fmt.Println("get:", s[1])
    fmt.Println("len:", len(s))

    s = append(s, "d")
    s = append(s, "e", "f")
    fmt.Println("apd:", s)

    c := make([]string, len(s))
    copy(c, s)
    fmt.Println("cpy:", c)

    l := s[2:5]
    fmt.Println("sl1:", l)
}

Puntos clave:

  • make([]T, n) crea un slice de longitud n (y capacidad n por defecto), listo para usar sin declarar tamaño en el tipo.
  • append agrega elementos al final; si el slice subyacente no tiene capacidad suficiente, Go reserva un array nuevo más grande por detrás y copia los datos — por eso append siempre devuelve el slice resultante, que hay que reasignar.
  • copy(destino, origen) copia elementos entre slices sin compartir el array subyacente.
  • La sintaxis de “slicing” s[bajo:alto] (como s[2:5]) devuelve una vista sobre el mismo array subyacente, no una copia — modificar el resultado puede modificar el slice original si comparten memoria.
  • Se puede declarar un slice literal directamente: s := []int{1, 2, 3} (sin número entre corchetes, a diferencia de un array).
  • Los slices también pueden ser multidimensionales, con cada slice interno de longitud independiente (a diferencia de los arrays anidados, que son rectangulares por definición).
  • Los slices son de referencia en la práctica: pasarlos a una función no copia los datos subyacentes, solo la cabecera (puntero, longitud, capacidad). Esta es la diferencia de comportamiento más importante frente a arrays y la razón por la que son el tipo por defecto para trabajar con colecciones en Go.

3. Maps

Un map es la estructura clave-valor de Go, equivalente a un diccionario o hash map en otros lenguajes.

package main

import "fmt"

func main() {
    m := make(map[string]int)
    m["k1"] = 7
    m["k2"] = 13
    fmt.Println("map:", m)

    v1 := m["k1"]
    fmt.Println("v1:", v1)

    fmt.Println("len:", len(m))

    delete(m, "k2")
    fmt.Println("map:", m)

    _, prs := m["k2"]
    fmt.Println("prs:", prs)

    n := map[string]int{"foo": 1, "bar": 2}
    fmt.Println("map:", n)
}

Puntos clave:

  • make(map[K]V) crea un map vacío listo para usar.
  • Acceder a una clave que no existe no genera error: devuelve el valor cero del tipo (0 para int, "" para string, etc.), lo que puede ocultar bugs si no se valida explícitamente.
  • La forma valor, presente := m[clave] es la manera correcta de distinguir “la clave existe con valor cero” de “la clave no existe”: presente es un booleano.
  • delete(m, clave) elimina una entrada; si la clave no existe, no hace nada (no da error).
  • len(m) devuelve el número de pares clave-valor.
  • Los maps también admiten declaración literal: map[string]int{"foo": 1, "bar": 2}.
  • El orden de iteración sobre un map no está garantizado y de hecho varía entre ejecuciones a propósito (ver siguiente punto).

4. Range over Built-in Types

range es la construcción que Go usa para iterar sobre arrays, slices, strings y maps, con una sintaxis uniforme pero un significado distinto según el tipo.

package main

import "fmt"

func main() {
    nums := []int{2, 3, 4}
    suma := 0
    for i, num := range nums {
        suma += num
        if num == 3 {
            fmt.Println("índice:", i)
        }
    }
    fmt.Println("suma:", suma)

    kvs := map[string]string{"a": "manzana", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }

    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

Comportamiento según el tipo:

  • Arrays y slices: range devuelve el índice y el valor de cada elemento, en orden. Si solo necesitás el índice, se puede omitir el valor (for i := range nums); si solo necesitás el valor, se usa _ para el índice (for _, v := range nums).
  • Maps: range devuelve clave y valor, pero el orden es aleatorio a propósito en cada ejecución. Go lo hace así deliberadamente para que nadie dependa por accidente de un orden que no está garantizado por el lenguaje. Si necesitás orden, hay que extraer las claves, ordenarlas (por ejemplo con sort.Strings) e iterar sobre esa lista aparte.
  • Strings: range itera por rune (punto de código Unicode), no por byte. El índice que devuelve es la posición en bytes donde empieza ese rune dentro del string codificado en UTF-8, lo cual importa apenas hay caracteres fuera del rango ASCII (acentos, ñ, emojis) — el índice puede “saltar” más de uno entre iteraciones. Ver también Strings and Runes en gobyexample para el detalle completo de esta codificación.

Cómo se relacionan estos temas entre sí

  • Arrays son la base, slices son lo que realmente usás: un slice es una vista dinámica sobre un array subyacente. Entender arrays primero es lo que hace que el comportamiento de append, copy y el slicing (s[a:b]) de slices tenga sentido en vez de parecer magia.
  • Value vs. reference es la distinción que más importa en la práctica: arrays se copian al pasarlos, slices y maps se comparten por referencia (vía su cabecera interna). Esto determina si una función puede modificar la colección original del que la llama o no.
  • Maps resuelven lo que arrays/slices no pueden: acceso por clave arbitraria en vez de por índice numérico secuencial, a costa de no tener orden garantizado.
  • range es el pegamento: la misma palabra clave sirve para los tres tipos (y para channels, como viste en la guía de concurrencia), pero el número y significado de los valores que devuelve cambia según lo que estés recorriendo. Vale la pena memorizar la tabla: array/slice → índice+valor, map → clave+valor (desordenado), string → índice en bytes + rune.

Un ejercicio útil para fijar esto: tomá un slice de structs, convertilo a un map indexado por algún campo (por ejemplo un ID), y después recorré ese map ordenando las claves manualmente antes de iterar. Ese ejercicio toca los cuatro temas de este documento en una sola pasada.