В чем разница между потокобезопасной и не потокобезопасной мапой
Что проверяют
Понимаете ли вы, что встроенная map в Go не защищена от конкурентной записи, и что именно ломается при гонке. Следом почти всегда спрашивают, когда брать sync.Map, а когда обычную мапу с мьютексом.
Как отвечать
- обычная
map: одновременное чтение из многих горутин безопасно, но запись параллельно с любым другим доступом это гонка. Рантайм часто её ловит и роняет процесс сfatal error: concurrent map writes. Это не паника,recoverеё не перехватит, упадёт весь сервис. Обнаружение не гарантировано, поэтому гонки ищут черезgo test -race; mapплюсsync.Mutexилиsync.RWMutex: вариант по умолчанию. Типизирован, прозрачен и позволяет сделать составную операцию «проверить и записать» под одной блокировкой.RWMutexвыигрывает, только когда чтений намного больше записей;sync.Map: документация советует его для двух случаев: ключ пишется один раз и много читается (кэш) либо горутины работают с непересекающимися наборами ключей. С Go 1.24 он построен на конкурентном хэш-трае и стал быстрее на изменениях. Минусы: ключи и значения типаany, нужны приведения типов, нетLen;- шардированная мапа: N мап со своими мьютексами по хэшу ключа, когда один мьютекс становится узким местом.
Составные операции в sync.Map делаются методами LoadOrStore, LoadAndDelete, CompareAndSwap, а не парой Load и Store.
Где спотыкаются
Говорят, что sync.Map всегда лучше, раз он потокобезопасный. При частых обновлениях одних и тех же ключей мапа с мьютексом бывает быстрее и проще, решает бенчмарк.
Пишут Load, а при отсутствии ключа Store, и считают это безопасным. Каждый вызов атомарен, а связка нет: две горутины обе увидят пустоту и обе запишут.
Копируют структуру с мьютексом по значению, и блокировка перестаёт защищать общую мапу. Это ловит go vet проверкой copylocks, но только если его запускать в CI.