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) == 3Cuá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=3Modelo 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).
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
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 elementosTambié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 arrayIteració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"]++ // actualizarBú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 nadaRecorrer 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 UnicodeBytes 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 rangesobre un string decodifica runas.
s := "café"
bytes := len(s) // 5 (porque "é" ocupa 2 bytes en UTF-8)
countRunes := 0
for range s {
countRunes++
}
// countRunes == 4Indexar 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.Builderfuera del bucle. - 3) (Opcional) Llama a
Growsi puedes estimar el tamaño final. - 4) Usa
WriteString/WriteByte/WriteRuneen 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 cuando | Puntos clave |
|---|---|---|
Array [N]T | Tamaño fijo conocido | Se copia al asignar/pasar; tipo incluye N |
Slice []T | Longitud variable, colecciones comunes | Comparte array subyacente; len/cap; append puede realocar |
Map map[K]V | Búsqueda por clave | Orden no garantizado; v, ok; no concurrente sin sync |
String string | Texto/bytes inmutables | len en bytes; range decodifica runas UTF-8 |