Ответ
Стандартная 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 тип, оптимизированный для двух конкретных сценариев:
- Когда ключ записывается один раз, а затем читается много раз из разных горутин.
- Когда разные горутины работают с разными, непересекающимися наборами ключей.
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
, избегая блокировок и создавая более предсказуемую систему.