ProgrammationDéveloppeur Go

Quelles sont les subtilités de l'utilisation de map en Go à connaître pour éviter des erreurs implicites lors d'accès multithreads et de suppression d'éléments pendant l'itération ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse.

En Go, map par défaut n'est pas une structure de données thread-safe. L'écriture ou la modification simultanée d'une map par différentes goroutines entraîne des conditions de concurrence et des paniques de type "concurrent map writes". Pour protéger une map, on utilise généralement sync.Mutex, sync.RWMutex ou le type spécial sync.Map de la bibliothèque standard, qui implémente des opérations atomiques sûres.

On peut supprimer des éléments d'une map via delete(map, key), mais lors de l'itération sur la map (avec range), il y a des nuances à prendre en compte :

  • On peut supprimer en toute sécurité l'élément de l'itération en cours.
  • On peut ajouter de nouveaux éléments à la map pendant un range, mais il n'est pas garanti que les nouvelles clés soient traitées dans l'itération actuelle.
  • Modifier la map depuis une autre goroutine pendant l'itération est une erreur (condition de concurrence ou panic).

Exemple de travail en toute sécurité avec 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() }

Question piégeuse.

Question : Peut-on supprimer en toute sécurité des éléments d'une map pendant une itération range sur cette même map ?

Réponse : Oui, uniquement si l'on supprime la clé qui a déjà été retournée par l'itération range. Mais il ne faut pas modifier la map depuis d'autres goroutines en parallèle : cela entraînerait une condition de concurrence.

m := map[string]int{"a": 1, "b": 2, "c": 3} for k := range m { delete(m, k) // sûr ! (si fait uniquement depuis cette goroutine) }

Exemples d'erreurs réelles dues à une méconnaissance des subtilités du sujet.


Histoire

Un des services de surveillance tenait des statistiques sur une map et se mettait à jour instantanément depuis plusieurs goroutines (compteurs de métriques). À un moment de pointe, des paniques "concurrent map writes" sont survenues, le service s'est arrêté et les données ont été perdues. Solution : ajouter un mutex ou utiliser sync.Map au lieu d'une map ordinaire.


Histoire

Lors de la migration des données, quelqu'un a décidé d'accélérer le nettoyage d'une grande map à l'aide de goroutines parallèles, chacune supprimant sa partie de clés via range. Au final, des data races constantes et des plantages imprévisibles. Après la migration, il a fallu revenir à un nettoyage séquentiel ou bloquer l'accès pendant un range.


Histoire

Dans une boucle range sur une map, un développeur ajoutait de nouvelles données dans la même map (par exemple, il formait une liste de nœuds adjacents pour un graphique). Il s'est avéré que les nouvelles clés pouvaient être ignorées dans l'itération range actuelle, ce qui a conduit à un traitement incomplet du graphique. Le bug n'a été découvert qu'à l'issue de tests complets de cas rares. Après correction, l'algorithme a été réécrit en utilisant une file d'attente séparée pour les ajouts.