Как передача мьютекса по значению влияет на защиту от гонок в Go?
Что проверяют
Понимаете ли вы, что sync.Mutex это обычная структура с состоянием, и её копия никак не связана с оригиналом. Следом обычно спрашивают, как поймать такую ошибку до продакшена и почему копия захваченного мьютекса может привести к дедлоку.
Как отвечать
При передаче по значению Go копирует структуру целиком вместе с мьютексом. Горутины, которые берут Lock на разных копиях, друг друга не ждут, и общие данные остаются без защиты. Если мьютекс был захвачен в момент копирования, копия тоже окажется захваченной, и первый же Lock на ней повиснет навсегда.
Где копирование происходит незаметно:
- получатель по значению:
func (c Counter) Inc()блокирует копию, поэтому у типа с мьютексом все методы должны быть на указателеfunc (c *Counter); - передача структуры в функцию:
process(cfg Config), где внутриConfigлежитsync.Mutex; - range по срезу структур:
for _, item := range itemsдаёт копию каждого элемента, работать нужно черезitems[i]или срез указателей; - присваивание и возврат:
c2 := *c1или функция, которая возвращает структуру по значению.
Документация пакета sync прямо говорит: мьютекс нельзя копировать после первого использования. То же относится к sync.WaitGroup, sync.Once и sync.Cond.
Статически такие места находит go vet с анализатором copylocks, он пишет «passes lock by value» или «assignment copies lock value». Саму гонку в работе покажет go test -race.
Где спотыкаются
Смешивают получатели: часть методов на указателе, часть на значении. Код компилируется, тесты без -race проходят, а один метод работает с копией.
Считают, что это вопрос производительности. Это вопрос корректности: защита не становится медленнее, она исчезает полностью.
Встраивают sync.Mutex в экспортируемую структуру анонимным полем. Методы Lock и Unlock становятся частью публичного API типа, и внешний код начинает брать блокировку сам. Надёжнее неэкспортируемое поле mu sync.Mutex рядом с данными, которые оно защищает.