Урок по треку Go 7 мин

sync и атомики

Mutex и RWMutex, WaitGroup, Once, sync.Map, Pool, Cond, atomic, гонки данных и race detector.

Каналы хороши для передачи данных, а для защиты общего состояния проще мьютекс. Пакет 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.

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.