Collections en Go: Arrays, Slices y Maps Explicados
En los posts anteriores exploramos structs, interfaces y métodos en Go. Hoy vamos a profundizar en las estructuras de datos fundamentales: arrays, slices y maps. Si vienes de Java, encontrarás similitudes con ArrayList y HashMap, pero también diferencias importantes que debes entender.
Arrays: Tamaño Fijo y Valores Primitivos
En Go, los arrays son diferentes a los de Java. Son valores primitivos que se copian completamente cuando los pasas a funciones.
// Arrays tienen tamaño fijo
var arr1 [5]int // [0 0 0 0 0]
arr2 := [5]int{1, 2, 3, 4, 5} // [1 2 3 4 5]
arr3 := [...]int{1, 2, 3} // El compilador cuenta: [1 2 3]
Diferencias Clave con Java
| Aspecto | Java | Go |
| Tipo | Objeto (referencia) | Valor primitivo |
| Pasar a función | Se pasa referencia | Se copia completamente |
| Tamaño | Puede cambiar | Fijo en tiempo de compilación |
// En Go, los arrays se copian por valor
arr2 := [5]int{1, 2, 3, 4, 5}
arr4 := arr2
arr4[0] = 999
fmt.Println(arr2) // [1 2 3 4 5] - No cambió
fmt.Println(arr4) // [999 2 3 4 5] - Sí cambió
En Java, esto sería diferente porque los arrays son objetos y se pasan por referencia.
¿Cuándo Usar Arrays?
Los arrays en Go se usan raramente porque:
Tamaño fijo es limitante
Las copias completas pueden ser costosas
Los slices son más flexibles
Usa arrays cuando:
Necesitas tamaño fijo garantizado en tiempo de compilación
Trabajas con estructuras de datos de tamaño conocido (ej: matrices 3x3)
Slices: La Estrella de Go
Los slices son lo que realmente usarás el 99% del tiempo en Go. Son similares a ArrayList en Java, pero más eficientes y con características únicas.
Crear Slices
var s1 []int // nil slice
s2 := []int{} // slice vacío (no nil)
s3 := []int{1, 2, 3, 4, 5} // slice con valores iniciales
s4 := make([]int, 5) // slice de longitud 5, capacidad 5
s5 := make([]int, 0, 10) // slice de longitud 0, capacidad 10
Len vs Cap: Conceptos Fundamentales
Esta es una diferencia importante con Java:
len: Número de elementos actuales en el slicecap: Capacidad del array subyacente (cuántos elementos puede contener sin reasignar memoria)
slice := make([]int, 3, 10) // len=3, cap=10
fmt.Printf("len: %d, cap: %d\n", len(slice), cap(slice))
// Output: len: 3, cap: 10
Piensa en len como "cuántos elementos tengo" y cap como "cuántos elementos puedo tener sin reasignar memoria".
Append: Agregar Elementos
s6 := []int{1, 2, 3}
fmt.Printf("Before: len=%d, cap=%d\n", len(s6), cap(s6))
// Before: len=3, cap=3
s6 = append(s6, 4, 5, 6)
fmt.Printf("After: len=%d, cap=%d\n", len(s6), cap(s6))
// After: len=6, cap=6 (o más, dependiendo del crecimiento)
Importante: append puede crear un nuevo array si excede la capacidad. Por eso siempre asignas el resultado: slice = append(slice, value).
Slicing: Crear Sub-slices
Los slices pueden crear "vistas" de otros slices:
original := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
slice1 := original[2:5] // [2 3 4] - índices 2 a 4 (no incluye 5)
slice2 := original[:5] // [0 1 2 3 4] - desde inicio hasta índice 4
slice3 := original[5:] // [5 6 7 8 9] - desde índice 5 hasta el final
⚠️ Trampa Común: Slices Comparten Memoria
Esto es importante: Los slices creados mediante slicing comparten el array subyacente:
original := []int{0, 1, 2, 3, 4, 5}
slice := original[2:5] // [2 3 4]
slice[0] = 999
fmt.Println(original) // [0 1 999 3 4 5] - ¡Cambió!
fmt.Println(slice) // [999 3 4]
Solución: Si necesitas una copia independiente, usa copy():
copySlice := make([]int, len(original))
copy(copySlice, original)
copySlice[0] = 111
fmt.Println(original) // [0 1 2 3 4 5] - No cambió
fmt.Println(copySlice) // [111 1 2 3 4 5]
Operaciones Comunes con Slices
numbers := []int{5, 2, 8, 1, 9, 3}
// Ordenar
sorted := make([]int, len(numbers))
copy(sorted, numbers)
sort.Ints(sorted)
// Filtrar (elementos mayores que 5)
filtered := make([]int, 0)
for _, n := range numbers {
if n > 5 {
filtered = append(filtered, n)
}
}
// Mapear (duplicar valores)
mapped := make([]int, len(numbers))
for i, n := range numbers {
mapped[i] = n * 2
}
// Reducir (suma)
sum := 0
for _, n := range numbers {
sum += n
}
Maps: Estructuras Clave-Valor
Los maps en Go son similares a HashMap en Java, pero con algunas diferencias importantes.
Crear Maps
var m1 map[string]int // nil map (no se puede usar hasta inicializar)
m2 := make(map[string]int) // map vacío inicializado
m3 := map[string]int{ // map con valores iniciales
"apple": 5,
"banana": 3,
"orange": 2,
}
Importante: Un map nil no se puede usar. Siempre inicialízalo con make() o con valores iniciales.
Acceder a Valores
value := m3["apple"] // 5
Verificar si una Clave Existe
Esta es una característica única de Go:
value, exists := m3["grape"]
if exists {
fmt.Printf("grape exists: %d\n", value)
} else {
fmt.Println("grape does not exist")
}
El segundo valor (exists) te dice si la clave está presente. Si no existe, value será el valor cero del tipo (0 para int, "" para string, etc.).
Eliminar Elementos
delete(m3, "banana")
Iterar sobre Maps
for key, value := range m3 {
fmt.Printf("%s: %d\n", key, value)
}
Nota: El orden de iteración en maps es aleatorio en Go. No confíes en un orden específico.
Obtener Claves o Valores
keys := make([]string, 0, len(m3))
values := make([]int, 0, len(m3))
for k, v := range m3 {
keys = append(keys, k)
values = append(values, v)
}
Restricciones de Maps
Las claves de un map deben ser comparables:
✅
string,int,float64, etc.❌
slice,map,func(no son comparables)
Ejemplo Práctico: Sistema de Inventario
Vamos a construir un sistema de inventario que demuestra el uso de slices y maps:
type Product struct {
ID string
Name string
Price float64
Stock int
}
type Inventory struct {
products map[string]*Product // Map para búsqueda rápida por ID
orders []string // Slice para mantener orden
}
func NewInventory() *Inventory {
return &Inventory{
products: make(map[string]*Product),
orders: make([]string, 0),
}
}
func (inv *Inventory) AddProduct(id, name string, price float64, stock int) {
inv.products[id] = &Product{
ID: id,
Name: name,
Price: price,
Stock: stock,
}
inv.orders = append(inv.orders, id) // Mantener orden de inserción
}
func (inv *Inventory) GetProduct(id string) (*Product, bool) {
product, exists := inv.products[id]
return product, exists
}
func (inv *Inventory) ListProducts() []*Product {
// Crear slice con capacidad pre-asignada para mejor performance
products := make([]*Product, 0, len(inv.products))
for _, id := range inv.orders {
if product, exists := inv.products[id]; exists {
products = append(products, product)
}
}
return products
}
Este ejemplo muestra:
Map para búsqueda rápida O(1) por ID
Slice para mantener el orden de inserción
Pre-asignación de capacidad con
make([]*Product, 0, len(inv.products))para mejor performance
Comparación: Java vs Go
| Característica | Java | Go |
| Lista dinámica | ArrayList<T> | []T (slice) |
| Map clave-valor | HashMap<K, V> | map[K]V |
| Array fijo | T[] (objeto) | [N]T (valor) |
| Verificar existencia | containsKey() | value, exists := m[key] |
| Orden garantizado | LinkedHashMap | No (usa slice + map) |
| Nil/Null | null | nil |
Mejores Prácticas
1. Pre-asigna Capacidad cuando Sepas el Tamaño
// ✅ Bueno: Pre-asigna capacidad
products := make([]*Product, 0, expectedSize)
// ❌ Menos eficiente: Crece dinámicamente
products := make([]*Product, 0)
2. Usa make() con Capacidad para Maps Grandes
// Si sabes que tendrás ~100 elementos
m := make(map[string]int, 100)
3. Siempre Verifica Existencia en Maps
// ✅ Bueno
value, exists := m[key]
if exists {
// usar value
}
// ❌ Puede ser problemático si 0 es un valor válido
value := m[key] // ¿Es 0 porque no existe o porque el valor es 0?
4. Copia Slices cuando Necesites Independencia
// Si necesitas modificar sin afectar el original
copy := make([]int, len(original))
copy(copy, original)
5. Usa Slices en Lugar de Arrays
// ✅ Usa slices (más común)
func processItems(items []string) { ... }
// ❌ Evita arrays (menos flexible)
func processItems(items [10]string) { ... }
Errores Comunes
❌ Error 1: Usar Map Nil
var m map[string]int
m["key"] = 1 // ❌ PANIC! map is nil
// ✅ Solución
m := make(map[string]int)
m["key"] = 1
❌ Error 2: Modificar Slice Compartido Sin Quererlo
original := []int{1, 2, 3}
slice := original[1:3]
slice[0] = 999
fmt.Println(original) // [1 999 3] - ¡Cambió!
// ✅ Solución: Copia si necesitas independencia
copy := make([]int, len(original))
copy(copy, original)
❌ Error 3: Olvidar Asignar Resultado de append
slice := []int{1, 2, 3}
append(slice, 4) // ❌ No hace nada, slice sigue siendo [1 2 3]
// ✅ Solución
slice = append(slice, 4) // slice ahora es [1 2 3 4]
Conclusiones
Las collections en Go son poderosas pero tienen características únicas:
✅ Arrays: Raramente usados, tamaño fijo, se copian por valor
✅ Slices: La estructura más común, dinámicos, eficientes, comparten memoria
✅ Maps: Similares a HashMap, pero con sintaxis más simple y verificación de existencia integrada
Conceptos clave a recordar:
lenvscapen slicesLos slices comparten el array subyacente
Siempre verifica existencia en maps con el segundo valor de retorno
Pre-asigna capacidad cuando sepas el tamaño aproximado
Una vez que entiendas estos conceptos, trabajar con collections en Go será natural y eficiente.
Próximos Pasos
En el siguiente post exploraremos el manejo de errores en Go. Aprenderemos sobre errors.Is(), errors.As(), errors.Join(), y el wrapping de errores. Este es un tema fundamental que diferencia Go de lenguajes con excepciones como Java.
¿Tienes preguntas sobre arrays, slices o maps en Go? Déjame saber en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.
Enjoy!
José Díaz
+51 939965148




