sync.Map în Go

Un map sigur pentru goroutine fără mutex extern: Store, Load, Delete, Range și Clear pot fi apelate concurent.

Sintaxă
var m sync.Map
func (m *Map) Store(key, value any)
func (m *Map) Load(key any) (value any, ok bool)
func (m *Map) Delete(key any)
func (m *Map) Range(f func(key, value any) bool)
func (m *Map) Clear()   // Go 1.23

Ce face sync.Map

Un map obișnuit nu poate fi scris din mai multe goroutine în același timp (programul se oprește cu fatal error: concurrent map writes). sync.Map poate: toate metodele lui sunt sigure pentru folosire concurentă. Valoarea zero este un map gol, gata de folosit.

Store(k, v) pune o valoare, Load(k) o citește (cu ok = dacă există), Delete(k) o șterge, Clear() golește tot, iar Range(f) parcurge perechile până când f returnează false. Cheile și valorile sunt de tip any, deci la citire ai nevoie de o conversie (v.(int)). Ordinea din Range nu este garantată.

Documentația recomandă sync.Map în două cazuri: chei scrise o dată și citite de multe ori (cache care doar crește) sau goroutine care lucrează pe chei diferite. În celelalte cazuri, un map obișnuit protejat de sync.Mutex are tipuri sigure și este la fel de rapid.

Ce returnează

Load: valoarea (any) și ok. Store, Delete, Range, Clear nu returnează nimic.

Exemple

Exemplul 1: Operațiile de bază

Cheile din Range sunt adunate și sortate, pentru că ordinea parcurgerii nu este fixă.

package main

import (
	"fmt"
	"slices"
	"sync"
)

func main() {
	var stoc sync.Map
	stoc.Store("mere", 120)
	stoc.Store("pere", 45)
	stoc.Store("prune", 80)

	if v, ok := stoc.Load("mere"); ok {
		fmt.Println("mere:", v.(int), "kg")
	}
	_, ok := stoc.Load("banane")
	fmt.Println("banane există:", ok)

	stoc.Delete("pere")

	var chei []string
	stoc.Range(func(k, v any) bool {
		chei = append(chei, fmt.Sprintf("%s=%d", k, v))
		return true // continuă parcurgerea
	})
	slices.Sort(chei)
	fmt.Println("Stoc:", chei)

	stoc.Clear()
	n := 0
	stoc.Range(func(k, v any) bool { n++; return true })
	fmt.Println("După Clear:", n, "produse")
}

Rezultat

mere: 120 kg
banane există: false
Stoc: [mere=120 prune=80]
După Clear: 0 produse

Exemplul 2: Scrieri din multe goroutine

Fiecare goroutine scrie cheia lui. Cu un map obișnuit fără mutex, programul s-ar putea opri cu eroare fatală.

package main

import (
	"fmt"
	"sync"
)

func main() {
	var patrate sync.Map
	var wg sync.WaitGroup
	for i := 1; i <= 50; i++ {
		wg.Go(func() {
			patrate.Store(i, i*i)
		})
	}
	wg.Wait()

	suma := 0
	patrate.Range(func(_, v any) bool {
		suma += v.(int)
		return true
	})
	v, _ := patrate.Load(7)
	fmt.Println("7² =", v)
	fmt.Println("Suma pătratelor 1..50 =", suma)
}

Rezultat

7² = 49
Suma pătratelor 1..50 = 42925

Greșeli frecvente

  • Faci v.(int) pe o valoare salvată ca int64 sau string: programul se oprește cu panică. Folosește forma sigură n, ok := v.(int).
  • Faci Load și apoi Store ca să „adaugi doar dacă lipsește”. Între cele două apeluri alt goroutine poate scrie; folosește LoadOrStore.
  • Copiezi un sync.Map după folosire (îl transmiți prin valoare). Transmite *sync.Map.

Comenzi înrudite

  • sync.Map.LoadOrStore: Operații combinate fără cursă de date: LoadOrStore pune doar dacă lipsește, LoadAndDelete scoate, Swap înlocuiește.
  • sync.Map.CompareAndSwap: Schimbă (CompareAndSwap) sau șterge (CompareAndDelete) o cheie doar dacă valoarea ei este încă cea așteptată.
  • sync.RWMutex: Lacăt pentru citire și scriere: mai mulți cititori pot lucra deodată (RLock), dar un scriitor lucrează singur (Lock).
  • sync.Mutex: Lacăt de excludere reciprocă: doar un goroutine pe rând poate rula codul dintre Lock() și Unlock().