ProgramaciónDesarrollador Go

¿Cuáles son las sutilezas del trabajo con map en Go que se deben conocer para evitar errores implícitos durante el acceso multihilo y la eliminación de elementos durante la iteración?

Supere entrevistas con el asistente de IA Hintsage

Respuesta.

En Go, map por defecto no es seguro para subprocesos. La escritura simultánea o la modificación de un map desde diferentes gorutinas conduce a condiciones de carrera y pánicos del tipo "concurrent map writes". Para proteger el map, se suelen utilizar sync.Mutex, sync.RWMutex o el tipo especial sync.Map de la biblioteca estándar, que implementa operaciones atómicas seguras.

Los elementos se pueden eliminar del map utilizando delete(map, key), sin embargo, durante la iteración sobre el map (a través de range) surgen matices:

  • Se puede eliminar de forma segura el elemento de la iteración actual.
  • Se pueden agregar nuevos elementos al map durante el range, pero no se garantiza que las nuevas claves sean procesadas en la iteración actual.
  • Modificar el map desde otra gorutina durante la iteración es un error (conditions de carrera o pánico).

Ejemplo de trabajo seguro con map:

type SafeMap struct { mu sync.RWMutex m map[string]int } func (s *SafeMap) Load(key string) (int, bool) { s.mu.RLock() v, ok := s.m[key] s.mu.RUnlock() return v, ok } func (s *SafeMap) Store(key string, value int) { s.mu.Lock() s.m[key] = value s.mu.Unlock() }

Pregunta capciosa.

Pregunta: ¿Se pueden eliminar elementos de forma segura de un map durante una iteración range sobre ese mismo map?

Respuesta: Sí, solo si se elimina la clave que ya ha sido procesada en la iteración range. Pero no se puede modificar el map desde otras gorutinas al mismo tiempo: esto causará una carrera.

m := map[string]int{"a": 1, "b": 2, "c": 3} for k := range m { delete(m, k) // ¡seguro! (si se hace solo desde esta gorutina) }

Ejemplos de errores reales debido a la falta de conocimiento sobre las sutilezas del tema.


Historia

Uno de los servicios de monitoreo mantenía estadísticas sobre un map y se actualizaba inmediatamente desde varias gorutinas (contadores de métricas). En el momento pico, ocurrieron pánicos "concurrent map writes", el servicio se desconectaba y se perdían datos. Solución: agregar un mutex o usar sync.Map en lugar de un map común.


Historia

Durante la migración de datos, alguien decidió acelerar la limpieza de un gran map utilizando gorutinas paralelas, cada una de las cuales eliminaba su parte de claves a través de range. Como resultado, se produjeron constantes carreras de datos y bloqueos impredecibles. Después de la migración, fue necesario regresar a una limpieza secuencial o bloquear el acceso durante el range.


Historia

En un ciclo range sobre un map, el desarrollador añadía nuevos datos al mismo map (por ejemplo, generando una lista de nodos adyacentes para un grafo). Resultó que las nuevas claves podían ser ignoradas en la iteración actual de range, lo que llevó a un procesamiento incompleto del grafo. El error solo fue descubierto durante la prueba exhaustiva de casos raros. Después de la corrección, el algoritmo fue reescrito utilizando una cola separada para las adiciones.