Каналы хороши для передачи данных, а для защиты общего состояния проще мьютекс. Пакет sync и sync/atomic дают инструменты для этого.
Гонка данных
Гонка это одновременный доступ двух горутин к одной памяти, где хотя бы один доступ на запись и нет синхронизации. counter++ из десяти горутин теряет инкременты: это чтение, сложение и запись, которые перемешиваются. Находит гонки детектор: go test -race, go run -race. Он замедляет программу в разы, поэтому включают его в тестах и CI, а не в проде.
Mutex и RWMutex
type Cache struct {
mu sync.RWMutex
m map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.m[k]
return v, ok
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[k] = v
}
- Мьютекс нельзя копировать после использования: передавайте структуру по указателю.
go vetловит копии. - Мьютекс в Go не реентерабельный: повторный
Lockв той же горутине это дедлок. RWMutexпускает много читателей одновременно, но если писатель ждёт, новые читатели встают в очередь. Выигрыш есть только при заметном перевесе чтений.- Мьютекс имеет нормальный режим и режим голодания: если горутина ждёт дольше 1 мс, мьютекс передаётся строго по очереди.
WaitGroup
var wg sync.WaitGroup
for _, u := range urls {
wg.Add(1)
go func() {
defer wg.Done()
fetch(u)
}()
}
wg.Wait()
Add вызывают до запуска горутины, а не внутри неё. С Go 1.25 есть wg.Go(func), который сам делает Add и Done.
Once, OnceValue
sync.Once выполняет функцию ровно один раз, даже при конкурентных вызовах: ленивая инициализация синглтона. Go 1.21 добавил sync.OnceFunc, OnceValue, OnceValues. Если функция внутри Once запаниковала, повторного вызова не будет.
sync.Map, Pool, Cond
sync.Mapоптимизирован под два сценария: ключ пишут один раз и много читают, или горутины работают с непересекающимися ключами. В остальных случаях обычная мапа с мьютексом проще и не медленнее.sync.Poolхранит переиспользуемые объекты (буферы) и снижает нагрузку на GC. Объекты в пуле могут исчезнуть при любой сборке мусора, это кеш, а не хранилище.sync.Condбудит ждущие горутины по условию (Wait,Signal,Broadcast). Используется редко, обычно хватает каналов.
Атомики
var hits atomic.Int64 // Go 1.19+: типизированные атомики
hits.Add(1)
n := hits.Load()
var cfg atomic.Pointer[Config]
cfg.Store(newCfg) // горячая замена конфига без мьютекса
Атомарные операции быстрее мьютекса для одного счётчика или флага и реализованы инструкциями процессора (например, LOCK XADD, CAS). Но атомарность одной переменной не защищает инвариант между двумя переменными: там нужен мьютекс. CompareAndSwap основа lock-free алгоритмов.
Модель памяти
Go гарантирует видимость записи другой горутине только через отношение happens-before: отправка в канал до получения, Unlock до следующего Lock, wg.Done до возврата Wait, атомарные операции синхронизируют как в последовательной согласованности. Без этого компилятор и процессор вправе переупорядочить запись, и «флаг done» в обычной переменной может так и не стать видимым.
Частые ошибки
- Копировать структуру с мьютексом (ресивер по значению).
- Забыть
Unlockна одной из веток: используйтеdefer. - Держать мьютекс во время медленного ввода-вывода.
- Атомарный счётчик рядом с обычной переменной, которые должны меняться вместе.
Что спросят на собесе
- Что такое гонка данных и как её найти.
- Mutex против RWMutex против каналов: когда что.
- Когда
sync.Mapлучше обычной мапы с мьютексом. - Чем атомики отличаются от мьютекса и как работает CAS.
- Что такое happens-before в модели памяти Go.