Cuando utilizas la sentencia for k, v := range m para recorrer un mapa, la iteración no es determinística; esto significa que el orden en que recibes los elementos puede variar entre ejecuciones. No es que el mapa sea caótico, sino que el runtime de Go inicia la iteración en un bucket (cubo) aleatorio de forma deliberada. Esta es una decisión de diseño para evitar que los desarrolladores dependan de un orden accidental que podría cambiar si el mapa crece, si cambian las colisiones de hash o si la implementación interna del runtime evoluciona en futuras versiones. Si tu lógica de negocio asume que un elemento aparecerá antes que otro basándose en su inserción o su clave, tu código es frágil. Si necesitas procesar los elementos en un orden específico, debes extraer las claves a un slice, ordenarlo y luego iterar sobre ese slice para consultar los valores en el mapa original. Si ignoras esta naturaleza y decides modificar el mapa mientras otra goroutine lo está leyendo, el programa lanzará un error fatal inmediato.
package main
import (
"fmt"
"sort"
)
func main() {
// Representamos una lista de usuarios y sus puntuaciones
puntuaciones := map[string]int{
"Zeta": 15,
"Alfa": 42,
"Milo": 88,
"Beta": 33,
"Delta": 55,
}
// 1. Iteración directa: El orden es impredecible.
// Aunque los datos sean los mismos, el orden de salida puede variar
// en cada ejecución debido a la aleatoriedad en el inicio del iterador.
fmt.Println("--- Iteración nativa (orden no determinístico) ---")
for usuario, puntos := range puntuaciones {
fmt.Printf("Usuario: %-6s | Puntos: %d\n", usuario, puntos)
}
// 2. Iteración ordenada: El patrón estándar para consistencia.
// Para garantizar un orden, proyectamos las llaves a un slice.
fmt.Println("\n--- Iteración ordenada (por nombre de usuario) ---")
// Pre-alocamos la capacidad del slice con el tamaño del mapa
// para evitar re-alocaciones innecesarias durante el append.
claves := make([]string, 0, len(puntuaciones))
for k := range puntuaciones {
claves = append(claves, k)
}
// Ordenamos el slice de strings de forma ascendente
sort.Strings(claves)
// Iteramos sobre el slice ordenado, usando las llaves para
// acceder de forma segura a los valores originales.
for _, k := range claves {
fmt.Printf("Usuario: %-6s | Puntos: %d\n", k, puntuaciones[k])
}
}
En el ejemplo anterior, la primera iteración usa el range directamente sobre puntuaciones. Aquí, el runtime de Go decide un punto de partida aleatorio en la estructura de datos del mapa, por lo que la salida no es predecible.
Para resolver la necesidad de orden, creamos un slice llamado claves. Es fundamental notar que usamos make([]string, 0, len(puntuaciones)); al pasarle la longitud del mapa como capacidad inicial, evitamos que el recolector de basura y el runtime tengan que redimensionar el slice varias veces mientras hacemos append. Una vez que tenemos todas las llaves, aplicamos sort.Strings(claves), que reorganiza los strings en el slice de forma determinista. Finalmente, no iteramos sobre el mapa, sino sobre claves, utilizando cada string k como llave para extraer el valor correcto desde puntuaciones[k]. Este patrón es el estándar de la industria para transformar la naturaleza desordenada de los mapas en una salida coherente.
El error frecuente
Un error crítico que ocurre en sistemas de alta concurrencia es intentar leer o iterar un mapa mientras otra goroutine lo está modificando (insertando o eliminando elementos). A diferencia de otros errores de lógica, esto no produce un comportamiento impredecible, sino un panic fatal que cierra el proceso completo:
// ESTO CAUSARÁ UN CRASH FATAL
go func() {
m["nueva_llave"] = 100 // Escritura concurrente
}()
for k, v := range m { // Lectura concurrente
fmt.Println(k, v)
}
// Error: fatal error: concurrent map iteration and map write
Si necesitas acceso compartido entre goroutines, utiliza sync.Mutex para proteger el mapa o la implementación sync.Map para casos de uso específicos de alta contención donde las llaves son mayormente estáticas.
N° 54