Estructuras de datos en Go: arrays, slices, mapas y cadenas

Capítulo 3

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

Arrays vs slices: qué son y cómo se representan

En Go, las colecciones más usadas para datos en secuencia son arrays y slices. Aunque se parecen al usarlos, su representación y comportamiento difieren y eso impacta en rendimiento, copias y mutabilidad.

Arrays: tamaño fijo y valor completo

Un array tiene longitud fija como parte de su tipo: [3]int no es compatible con [4]int. Al asignar o pasar un array a una función, se copia completo (salvo que uses puntero).

var a [3]int = [3]int{10, 20, 30} // len(a) == 3

Cuándo usar arrays: casos muy concretos donde el tamaño es fijo y pequeño (por ejemplo, buffers de tamaño constante) o cuando quieres semántica de copia explícita.

Slices: vista dinámica sobre un array subyacente

Un slice es una estructura ligera que apunta a un array subyacente y guarda len y cap. Su tipo es []T y su longitud puede variar. Al asignar un slice a otro, se copia el encabezado (puntero/len/cap), no los elementos; ambos pueden compartir el mismo array subyacente.

s := []int{10, 20, 30} // len=3, cap=3

Modelo mental: el slice es una “ventana” sobre un array. Cambiar elementos a través del slice cambia el array subyacente (y por tanto otros slices que lo compartan).

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

Longitud, capacidad, crecimiento y copias

len y cap

s := make([]int, 2, 5) // len=2 (elementos accesibles), cap=5 (espacio reservado)
  • len(s): número de elementos “activos”.
  • cap(s): capacidad antes de necesitar realocar el array subyacente.

append y crecimiento

append agrega elementos al final. Si hay capacidad suficiente, reutiliza el array subyacente; si no, crea uno nuevo (realoca), copia los elementos y devuelve un nuevo slice.

s := make([]int, 0, 2) // cap=2 para evitar realocaciones tempranas
s = append(s, 1)
s = append(s, 2)
s = append(s, 3) // aquí probablemente realoca (cap insuficiente)

Regla práctica: si conoces el tamaño aproximado final, preasigna capacidad para reducir asignaciones y copias.

Copias explícitas con copy

Para duplicar datos (no solo el encabezado), usa copy. Esto es clave cuando quieres evitar que dos slices compartan memoria.

src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src) // copia elementos

También puedes copiar subrangos:

src := []int{10, 20, 30, 40}
dst := make([]int, 2)
copy(dst, src[1:3]) // dst = {20, 30}

Operaciones comunes con slices

Slicing (sub-slices)

El slicing crea una nueva vista sobre el mismo array subyacente:

s := []int{0, 1, 2, 3, 4}
a := s[1:4] // {1,2,3}
b := s[:2] // {0,1}
c := s[3:] // {3,4}

Importante: a, b y c comparten memoria con s. Modificar a[0] modifica s[1].

Controlar capacidad al recortar (patrón anti-retención)

Un sub-slice puede retener en memoria un array grande aunque solo uses una parte pequeña. Para evitarlo, limita la capacidad con el slicing de 3 índices:

big := make([]byte, 0, 1_000_000)
// ... big crece ...
small := big[:100:100] // len=100, cap=100 (no puede crecer hacia el array grande)

Si necesitas que small no retenga el array grande, copia:

smallCopy := append([]byte(nil), small...) // copia a un nuevo array

Iteración: índices y valores

s := []string{"a", "b", "c"}
for i, v := range s {
_ = i
_ = v
}

Si solo necesitas el índice o el valor, usa _ para evitar variables no usadas:

for i := range s { _ = i }
for _, v := range s { _ = v }

Eliminar elementos (sin función built-in)

Eliminar en medio implica mover elementos. Dos patrones comunes:

  • Preservando orden (O(n)):
s := []int{10, 20, 30, 40}
i := 1 // eliminar el 20
s = append(s[:i], s[i+1:]...) // {10,30,40}
  • Sin preservar orden (O(1)):
s := []int{10, 20, 30, 40}
i := 1
s[i] = s[len(s)-1]
s = s[:len(s)-1] // {10,40,30}

Patrones de rendimiento para evitar asignaciones innecesarias

1) Preasignar con make cuando conoces el tamaño

n := 10_000
s := make([]int, 0, n)
for i := 0; i < n; i++ {
s = append(s, i)
}

2) Reutilizar buffers

Cuando procesas en bucles, reutiliza slices reseteando longitud a cero (manteniendo capacidad):

buf := make([]byte, 0, 4096)
for {
buf = buf[:0] // reutiliza memoria
// llenar buf de nuevo...
break
}

3) Evitar conversiones y copias innecesarias

Convertir entre []byte y string crea copias (por seguridad e inmutabilidad). Hazlo solo cuando sea necesario y, si el flujo lo permite, procesa en []byte durante parsing y convierte al final.

4) Cuidado con append sobre sub-slices compartidos

Si haces append a un sub-slice que aún tiene capacidad, puedes sobrescribir elementos “más allá” del sub-slice pero dentro del array compartido, afectando a otros slices. Si necesitas aislamiento, fuerza copia:

base := []int{1, 2, 3, 4}
sub := base[:2] // len=2, cap=4
safe := append([]int(nil), sub...) // copia
safe = append(safe, 99)

Mapas (maps): creación, búsqueda segura, borrado y recorrido

Crear mapas

Un mapa asocia claves a valores: map[K]V. Se crea con make o literal.

m1 := make(map[string]int)
m2 := map[string]int{"a": 1, "b": 2}

Si esperas muchas inserciones, puedes dar una capacidad inicial aproximada (no es un límite, es una pista):

m := make(map[string]int, 10_000)

Insertar y actualizar

m := make(map[string]int)
m["go"] = 1
m["go"]++ // actualizar

Búsqueda segura (comma ok)

Acceder a una clave inexistente devuelve el valor cero del tipo, lo cual puede ser ambiguo. Usa el patrón value, ok:

m := map[string]int{"a": 10}
v, ok := m["b"]
if !ok {
// no existe
_ = v // v es 0 aquí, pero no significa que exista
}

Borrar claves

m := map[string]int{"a": 1, "b": 2}
delete(m, "b") // si no existe, no pasa nada

Recorrer mapas y consideraciones de orden

El recorrido con range no garantiza orden; puede variar entre ejecuciones. Si necesitas orden, extrae las claves, ordénalas y luego accede:

m := map[string]int{"c": 3, "a": 1, "b": 2}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
// sort.Strings(keys) // requiere importar sort
for _, k := range keys {
_ = m[k]
}

Mapas y concurrencia

Un map no es seguro para acceso concurrente sin sincronización. Si varios goroutines escriben/leen simultáneamente, usa un sync.Mutex o sync.Map según el caso de uso.

Cadenas (strings) y runas: UTF-8 en la práctica

String: bytes inmutables

En Go, string es una secuencia inmutable de bytes. Normalmente contiene texto UTF-8, pero puede contener cualquier byte. La inmutabilidad implica que “modificar” un string crea uno nuevo.

s := "hola"
// s[0] es un byte (uint8), no un carácter Unicode

Bytes vs runas (rune)

rune es un alias de int32 y representa un punto de código Unicode. En UTF-8, un carácter puede ocupar 1 a 4 bytes. Por eso:

  • len(s) devuelve bytes, no “número de letras”.
  • for range sobre un string decodifica runas.
s := "café"
bytes := len(s) // 5 (porque "é" ocupa 2 bytes en UTF-8)
countRunes := 0
for range s {
countRunes++
}
// countRunes == 4

Indexar strings con cuidado

s[i] devuelve un byte. Si cortas un string por índices arbitrarios, puedes partir una secuencia UTF-8 a la mitad y obtener texto inválido. Si necesitas cortar por “caracteres”, trabaja con runas:

r := []rune("café")
sub := string(r[:3]) // "caf"

Esto crea asignaciones (conversión a []rune), así que úsalo cuando realmente necesites semántica por runas.

Construcción eficiente de strings con strings.Builder

Concatenar en bucle con + puede generar muchas asignaciones. Para construir strings incrementalmente, usa strings.Builder.

var b strings.Builder
b.Grow(64) // opcional: reserva aproximada
b.WriteString("user=")
b.WriteString("ana")
b.WriteByte('&')
b.WriteString("active=true")
out := b.String()

Guía paso a paso para reemplazar concatenación en bucle:

  • 1) Identifica concatenaciones repetidas dentro de un for.
  • 2) Crea un strings.Builder fuera del bucle.
  • 3) (Opcional) Llama a Grow si puedes estimar el tamaño final.
  • 4) Usa WriteString/WriteByte/WriteRune en cada iteración.
  • 5) Al final, llama a String() una sola vez.

Casos típicos de parsing: patrones prácticos

1) Separar y limpiar campos (CSV simple o listas)

Para entradas simples separadas por comas, strings.Split y strings.TrimSpace son directos. Si el volumen es alto, considera evitar splits repetidos y usar un escáner o parsing por índices.

line := "ana, bob, carla"
parts := strings.Split(line, ",")
names := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
names = append(names, p)
}
}

2) Parsear pares clave=valor a un map

Ejemplo típico: querystring simplificada a=1&b=2. Aquí se combinan slices, mapas y parsing numérico.

input := "a=1&b=2&debug=true"
m := make(map[string]string)
pairs := strings.Split(input, "&")
for _, pair := range pairs {
if pair == "" {
continue
}
kv := strings.SplitN(pair, "=", 2)
key := kv[0]
val := ""
if len(kv) == 2 {
val = kv[1]
}
m[key] = val
}
// acceso seguro
if v, ok := m["a"]; ok {
_ = v
}

3) Parsing eficiente por recorrido de bytes (sin Split)

Si quieres reducir asignaciones, puedes recorrer el string como bytes y extraer segmentos por índices. Este patrón evita crear muchos substrings intermedios si controlas cuándo convertir.

input := "a=1&b=2"
m := make(map[string]string)
start := 0
for i := 0; i <= len(input); i++ {
if i == len(input) || input[i] == '&' {
seg := input[start:i]
start = i + 1
if seg == "" {
continue
}
eq := strings.IndexByte(seg, '=')
if eq < 0 {
m[seg] = ""
continue
}
m[seg[:eq]] = seg[eq+1:]
}
}

Nota: los substrings comparten memoria con el string original. Si el string original es muy grande y vas a guardar muchos valores pequeños por mucho tiempo, considera copiar: valCopy := string(append([]byte(nil), val...)).

Tabla rápida: qué usar y cuándo

EstructuraÚsala cuandoPuntos clave
Array [N]TTamaño fijo conocidoSe copia al asignar/pasar; tipo incluye N
Slice []TLongitud variable, colecciones comunesComparte array subyacente; len/cap; append puede realocar
Map map[K]VBúsqueda por claveOrden no garantizado; v, ok; no concurrente sin sync
String stringTexto/bytes inmutableslen en bytes; range decodifica runas UTF-8

Ahora responde el ejercicio sobre el contenido:

¿Qué ocurre al asignar un slice a otra variable y luego modificar un elemento a través del nuevo slice?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Al asignar un slice, se copia su encabezado (puntero, longitud y capacidad), no los elementos. Si comparten el mismo array subyacente, modificar un elemento desde uno puede reflejarse en el otro.

Siguiente capítulo

Diseño con structs e interfaces en Go: composición y contratos

Arrow Right Icon
Portada de libro electrónico gratuitaGo desde Cero: Programación Moderna, Rápida y Escalable
25%

Go desde Cero: Programación Moderna, Rápida y Escalable

Nuevo curso

12 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.