Какие есть способы для безопасной конкурентной работы с map в Go?

Ответ

Стандартная map в Go не является потокобезопасной. Попытка одновременной записи или записи во время чтения из разных горутин приведет к панике. Существует три основных подхода для безопасной работы с map в конкурентной среде:

1. map с мьютексом (sync.RWMutex)

Это самый распространенный и гибкий подход. Вы защищаете доступ к map с помощью мьютекса.

  • sync.Mutex: для эксклюзивного доступа (и чтение, и запись).
  • sync.RWMutex: предпочтительнее, если операций чтения значительно больше, чем записей. Он позволяет неограниченному числу горутин одновременно читать данные (RLock), но запись (Lock) требует эксклюзивного доступа.
type SafeMap struct {
    mu   sync.RWMutex
    data map[string]int
}

func (sm *SafeMap) Set(key string, value int) {
    sm.mu.Lock() // Блокировка на запись
    defer sm.mu.Unlock()
    sm.data[key] = value
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock() // Блокировка на чтение
    defer sm.mu.RUnlock()
    val, ok := sm.data[key]
    return val, ok
}

Когда использовать: Когда нужна полная гибкость, сложные операции над map или когда производительность RWMutex для вашего сценария (много чтений) оптимальна.

2. sync.Map

Это встроенный в Go тип, оптимизированный для двух конкретных сценариев:

  1. Когда ключ записывается один раз, а затем читается много раз из разных горутин.
  2. Когда разные горутины работают с разными, непересекающимися наборами ключей.

sync.Map использует внутренние механизмы, чтобы избежать блокировок в этих случаях.

var m sync.Map
var wg sync.WaitGroup

for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        m.Store(i, i*i) // Безопасная запись
    }(i)
}
wg.Wait()

val, _ := m.Load(5) // Безопасное чтение
fmt.Println(val) // 25

Когда использовать: В сценариях "записал один раз, читай много" (например, кеши) или при работе с непересекающимися ключами.

3. Каналы (Идиоматичный Go)

Этот подход соответствует философии Go: "Не общайтесь, разделяя память; разделяйте память, общаясь".

Вместо того чтобы давать всем горутинам доступ к map и защищать ее мьютексом, вы создаете одну горутину, которая является единственным "владельцем" map. Все остальные горутины общаются с ней через каналы, отправляя запросы на чтение или запись.

// Концептуальный пример
requests := make(chan func(map[string]int))

go func() { // Горутина-владелец
    data := make(map[string]int)
    for req := range requests {
        req(data)
    }
}()

// Запись из другой горутины
requests <- func(m map[string]int) {
    m["key"] = 42
}

// Чтение из другой горутины
respChan := make(chan int)
requests <- func(m map[string]int) {
    respChan <- m["key"]
}
value := <- respChan

Когда использовать: Когда нужно инкапсулировать состояние и логику работы с map, избегая блокировок и создавая более предсказуемую систему.