Если мы только читаем из переменной, нужен ли мьютекс и какой именно
Что проверяют
Знаете ли вы модель памяти Go, а не правило «на всякий случай обернуть в мьютекс». Правильный ответ начинается с уточнения, пишет ли кто-то в переменную параллельно. Следом спрашивают про sync.RWMutex и его подводные камни.
Как отвечать
Разберите случаи:
- значение записано до запуска горутин и больше не меняется: синхронизация не нужна. По модели памяти всё, что сделано до оператора
go, гарантированно видно в запущенной горутине. Так работает конфигурация, прочитанная вmain()до старта серверов; - ленивая инициализация один раз:
sync.Onceилиsync.OnceValue(с Go 1.21); - кто-то пишет, пока другие читают: это гонка данных, синхронизация обязательна, даже если читателю «не страшно получить старое значение»;
sync.RWMutex: читатели входят одновременно черезRLock(), писатель черезLock()получает монопольный доступ. Выигрыш заметен, когда читателей много, а критическая секция длинная. На коротких секциях обычныйsync.Mutexчасто не медленнее;- атомики: для счётчиков и флагов
atomic.Int64иatomic.Bool, для снимка конфигурацииatomic.Pointer[T]. Писатель собирает новую структуру и подменяет указатель, читатели берутLoad()без блокировок.
Отдельно про map: одновременное чтение безопасно, но чтение вместе с записью приводит к фатальной ошибке concurrent map read and map write, которую не перехватить через recover. Гонки ищут флагом -race в тестах.
Где спотыкаются
Говорят, что чтение int на x86 атомарно, значит мьютекс не нужен. Модель памяти Go этого не обещает: компилятор вправе вынести чтение из цикла, и for !done {} не закончится никогда. А строка, срез и интерфейс занимают несколько машинных слов, их можно прочитать наполовину обновлёнными.
Берут повторный RLock() в той же горутине, например во вложенном вызове. Если между ними встал ожидающий писатель, второй RLock() заблокируется, и получится взаимоблокировка.
Копируют структуру с мьютексом по значению. Копия получает свой независимый замок, и защита пропадает. Это ловит go vet.