Как защититься от ошибки во время конкуррентной записи в map?go-7

Конкурентная запись в map без синхронизации — одна из самых опасных ошибок в Go, приводящая к неопределённому поведению и паникам. Рассмотрим профессиональные методы защиты.

1. Стандартные механизмы синхронизации

1.1. sync.Mutex

var (
    data = make(map[string]int)
    mu   sync.Mutex
)

func safeWrite(key string, value int) {
    mu.Lock()
    defer mu.Unlock()
    data[key] = value
}

Особенности:

  • Простая и надежная блокировка
  • defer гарантирует разблокировку даже при панике

1.2. sync.RWMutex

var (
    data = make(map[string]int)
    rwMu sync.RWMutex
)

func safeRead(key string) (int, bool) {
    rwMu.RLock()
    defer rwMu.RUnlock()
    val, ok := data[key]
    return val, ok
}

2. Специализированные структуры

2.1. sync.Map

var sm sync.Map

// Запись
sm.Store("key", 42)

// Чтение
if val, ok := sm.Load("key"); ok {
    fmt.Println(val)
}

Когда использовать:

  • Много горутин работают с разными ключами
  • Редкие обновления при частых чтениях
  • Неизвестный паттерн доступа

2.2. Атомарные указатели

import "sync/atomic"

var mapPtr atomic.Pointer[map[string]int]

func init() {
    m := make(map[string]int)
    mapPtr.Store(&m)
}

func updateMap(key string, value int) {
    newMap := make(map[string]int)
    oldMap := *mapPtr.Load()

    // Копируем старые данные
    for k, v := range oldMap {
        newMap[k] = v
    }

    // Добавляем новые
    newMap[key] = value

    // Атомарная замена
    mapPtr.Store(&newMap)
}

3. Паттерны для сложных случаев

3.1. Шардирование

const shards = 64

type ConcurrentMap struct {
    shards []*MapShard
}

type MapShard struct {
    data map[string]int
    mu   sync.RWMutex
}

func (cm *ConcurrentMap) Set(key string, value int) {
    shard := fnv32(key) % shards
    cm.shards[shard].mu.Lock()
    cm.shards[shard].data[key] = value
    cm.shards[shard].mu.Unlock()
}

3.2. Канальная модель

type MapOperation struct {
    key    string
    value  int
    action string // "set", "get", "delete"
    resp   chan interface{}
}

func mapManager(ops chan MapOperation) {
    data := make(map[string]int)
    for op := range ops {
        switch op.action {
        case "set":
            data[op.key] = op.value
            op.resp <- true
        case "get":
            op.resp <- data[op.key]
        }
    }
}

4. Инструменты обнаружения гонок

4.1. Флаг -race

go run -race main.go
go test -race ./...

4.2. Статический анализ

go vet
staticcheck

5. Опасные антипаттерны

  1. Ошибочная "оптимизация":
// НЕ ДЕЛАЙТЕ ТАК!
mu.RLock()
_, exists := data[key]
mu.RUnlock()

if !exists {
    mu.Lock()
    data[key] = value // Гонка данных!
    mu.Unlock()
}
  1. Глобальные переменные без защиты

  2. Копирование sync-примитивов

Резюмируем

  • Всегда используйте синхронизацию при конкурентном доступе
  • Выбирайте метод защиты в зависимости от паттерна доступа
  • sync.Map — хороший выбор для большинства случаев
  • Шардирование — для высоконагруженных систем
  • Канальная модель — для сложных транзакций
  • Обязательно проверяйте код на гонки
  • Избегайте ложных оптимизаций