Можно ли использовать atomic для изменения полей структуры в Go
Что проверяют
Понимаете ли вы границу атомарности: sync/atomic защищает одно машинное слово, а не структуру целиком. Следом обычно спрашивают, как атомарно обновить несколько связанных полей и когда проще взять sync.Mutex.
Как отвечать
Отдельное поле структуры менять атомарно можно, если оно подходящего типа. Функции пакета принимают указатель, а указатель на поле получить несложно: atomic.AddInt64(&s.hits, 1). С Go 1.19 удобнее типизированные обёртки, их объявляют прямо полем структуры:
atomic.Int64,atomic.Uint32,atomic.Bool: методыLoad,Store,Swap,CompareAndSwap, у числовых ещёAdd, никакого&и риска случайно прочитать поле без атомика;atomic.Pointer[T]: атомарная замена указателя на любую структуру;atomic.Value: старый вариант для произвольного значения, при каждомStoreтребует один и тот же конкретный тип.
Проблема начинается, когда поля связаны инвариантом. Если count и sum обновляются двумя атомарными операциями, читатель может увидеть новый count со старым sum. Каждая операция атомарна, пара нет. Здесь два пути:
- мьютекс:
sync.Mutexилиsync.RWMutexвокруг всех связанных полей; - copy-on-write: собрать новую структуру и опубликовать её через
atomic.Pointer[Stats], читатели делаютLoad()и видят согласованный снимок. Подходит для редко меняющихся данных вроде конфигурации.
Атомики дешевле мьютекса на простых счётчиках и флагах, но сложная логика на CAS-циклах быстро становится хрупкой.
Где спотыкаются
Смешивают атомарную запись с обычным чтением того же поля. Это гонка данных, её ловит go test -race, а типизированные atomic.Int64 такую ошибку просто не допускают.
Забывают про выравнивание. На 32-битных платформах функции вроде atomic.AddInt64 требуют адреса, выровненного по 64 битам, иначе паника. Типы atomic.Int64 и atomic.Uint64 выравниваются автоматически.
Копируют структуру с атомарными полями по значению, например передают её в функцию без указателя. Копия живёт отдельно от оригинала, и go vet на такое копирование ругается.