> serie: aprende go de 0 a 100 | artículo 8 de 19
Llevas siete artículos usando slices en Go sin saber qué son de verdad. Hoy abrimos el capó, porque la diferencia entre usar Go y entender Go está exactamente aquí, en saber qué hay debajo de un []int.
Arrays: los cimientos que casi nunca tocas
El array de Go es de tamaño fijo y se comporta como VALOR (A Tour of Go, arrays):
// Array: tamaño fijo, parte del tipo.
// [3]int y [4]int son tipos DISTINTOS.
var a [3]int
// Y se copia ENTERO al asignar o pasar a función.
b := a
b[0] = 99
fmt.Println(a[0]) // 0: a ni se enteró
Copia entera al asignar: nada que ver con Java o Python, donde pasas referencias. Por eso en el día a día casi nadie usa arrays directamente. Son los cimientos sobre los que viven los slices en Go.
Slices: tres campos y un array detrás
Un slice es una estructura diminuta con tres campos: un puntero al array subyacente, una longitud y una capacidad (Go Slices: usage and internals):
s := make([]int, 3, 5) // tipo, longitud, capacidad
fmt.Println(len(s), cap(s)) // 3 5
// append devuelve un slice NUEVO: reasigna siempre.
s = append(s, 42)
// Si cap se agota, Go crea otro array más grande
// y copia. Si no, reutiliza el mismo.
len es lo que ves, cap es cuánto puede crecer sin mudanza. Cuando append agota la capacidad, reserva un array mayor y copia. Por eso append devuelve el slice: puede apuntar a otra dirección (A Tour of Go, slices).

El gotcha del array compartido
Consecuencia directa de ese puntero interno, y fuente inagotable de bugs sutiles:
original := []int{1, 2, 3, 4, 5}
trozo := original[1:3] // [2 3]
trozo[0] = 99
fmt.Println(original) // [1 99 3 4 5] ¡sorpresa!
// Comparten array subyacente. Para independizarlos:
copia := make([]int, len(trozo))
copy(copia, trozo)
Trocear con s[a:b] no copia datos: crea otra cabecera mirando al MISMO array. Barato y rapidísimo, hasta que dos trozos se pisan. Cuando necesites independencia real, copy y a dormir tranquila.
Maps: el diccionario de serie
El map de Go es la tabla hash de toda la vida (A Tour of Go, maps y Effective Go):
edades := map[string]int{"ada": 36, "linus": 55}
// Leer una clave que no existe NO explota:
// devuelve el zero value. La coma ok distingue.
edad, ok := edades["grace"]
if !ok {
fmt.Println("no está grace")
}
delete(edades, "linus")
// Iterar: el orden NO está garantizado.
for nombre, edad := range edades {
fmt.Println(nombre, edad)
}
Tres cosas que te ahorrarán sustos:
- Leer clave inexistente devuelve zero value, no excepción, y la coma ok es tu amiga.
- Escribir en un map nil (declarado sin make ni literal) provoca panic.
- El orden de iteración no está especificado (especificación) y el runtime lo varía a propósito entre ejecuciones. Si tu código depende del orden de un map, tu código tiene un bug que aún no conoces.
Y el misterio del artículo 3: strings
Un string es una secuencia inmutable de BYTES en UTF-8, no de caracteres (Strings, bytes, runes and characters in Go):
s := "caña"
fmt.Println(len(s)) // 5, no 4: ñ ocupa 2 bytes
// range decodifica runas, no bytes:
for i, r := range s {
fmt.Printf("%d:%c ", i, r) // 0:c 1:a 2:ñ 4:a
}
Indexar con s[i] da bytes sueltos, pero range decodifica runa a runa y por eso los índices saltan (especificación, range). Misterio del reto del artículo 3 ¡resuelto!.
Reto de la semana: pon a prueba los slices en Go
- Predice en papel len y cap de un slice tras cinco append sucesivos partiendo de make([]int, 0, 2). Luego comprueba imprimiendo en cada paso.
- Reproduce el gotcha del array compartido y arréglalo con copy.
- Contador de palabras: lee un texto y construye un map[string]int con la frecuencia de cada palabra usando strings.Fields.
- Bonus: imprime «programación» byte a byte y runa a runa. Cuenta cuántos bytes ocupa la ó.
Slices en Go: nivel completado.
En el próximo artículo: manejo de errores idiomático. El famoso if err != nil y por qué a los gophers les gusta así.
> exit