Какие существуют способы синхронизации доступа к данным в Go?

Ответ

В Go есть несколько механизмов синхронизации, каждый из которых подходит для разных сценариев:

  1. Мьютексы (sync.Mutex) – используются для блокировки доступа к общему ресурсу, обеспечивая эксклюзивный доступ для одной горутины в любой момент времени. Идеально подходят для защиты структур данных.
var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    defer mu.Unlock() // Гарантирует разблокировку даже при панике
    counter++
}
  1. RWMutex (sync.RWMutex) – мьютекс для чтения/записи. Позволяет множественное чтение одновременно, но эксклюзивную запись. Оптимален, когда чтений значительно больше, чем записей.
var rw sync.RWMutex
var data map[string]string

func read(key string) string {
    rw.RLock()
    defer rw.RUnlock()
    return data[key]
}

func write(key, value string) {
    rw.Lock()
    defer rw.Unlock()
    data[key] = value
}
  1. Каналы (chan) – идиоматичный способ для горутин, позволяющий безопасно обмениваться данными и синхронизировать их выполнение, следуя принципу "не обменивайтесь данными путем совместного использования памяти; вместо этого обменивайтесь памятью путем коммуникации".
ch := make(chan int, 1) // Буферизованный канал

// Отправка данных
go func() {
    ch <- 42
}()

// Получение данных
val := <-ch
fmt.Println(val)
  1. Atomic операции (sync/atomic) – предоставляют низкоуровневые, атомарные операции для простых типов данных (целые числа, указатели). Используются для счетчиков или флагов, когда не требуется сложная логика блокировки.
var count int32
atomic.AddInt32(&count, 1) // Атомарное инкрементирование
val := atomic.LoadInt32(&count) // Атомарное чтение
  1. WaitGroup (sync.WaitGroup) – используется для ожидания завершения группы горутин. Позволяет основной горутине дождаться выполнения всех запущенных дочерних горутин.
var wg sync.WaitGroup

wg.Add(1) // Увеличиваем счетчик ожидаемых горутин
go func() {
    defer wg.Done() // Уменьшаем счетчик по завершении горутины
    // Выполнение работы горутины
    fmt.Println("Горутина завершила работу")
}()

wg.Wait() // Блокирует до тех пор, пока счетчик не станет равен 0
fmt.Println("Все горутины завершены")

Выбор конкретного механизма зависит от сценария: мьютексы для защиты сложных структур, каналы для коммуникации и координации между горутинами, atomic для простых счетчиков, а WaitGroup для ожидания завершения параллельных задач.