Примитивы синхронизации в Go
Каналы (channels)
Основной механизм синхронизации и коммуникации между горутинами. Каналы обеспечивают передачу данных и синхронизацию доступа.
Типы каналов:
- Небуферизированные — блокируют отправителя до получения данных получателем
- Буферизированные — блокируют только при заполнении буфера
ch := make(chan int) // небуферизированный
ch := make(chan int, 10) // буферизированный
Особенности:
- Закрытие канала через
close()— сигнал получателям о завершении - Чтение из закрытого канала возвращает zero value
- Запись в закрытый канал вызывает panic
- Каналы — ссылочный тип, сравнение только с nil
Мьютексы (sync.Mutex, sync.RWMutex)
Базовые блокировки для защиты разделяемых данных.
sync.Mutex:
Lock()/Unlock()— эксклюзивная блокировка- Не реентерабельный — повторный Lock в той же горутине вызывает deadlock
sync.RWMutex:
RLock()/RUnlock()— блокировка на чтение (множественный доступ)Lock()/Unlock()— блокировка на запись (эксклюзивный доступ)- Оптимизация для сценариев read-heavy
Типичные ошибки:
- Забыли вызвать Unlock (используйте defer)
- Копирование мьютекса после блокировки
- Неправильное использование RWMutex при частых записях
sync.WaitGroup
Ожидание завершения группы горутин.
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// работа
}()
}
wg.Wait()
Важно:
Add()должен вызываться до запуска горутины- Количество
Done()должно точно соответствоватьAdd() - WaitGroup можно передавать только по указателю
sync.Once
Гарантирует однократное выполнение функции (инициализация singleton, ленивая загрузка).
var once sync.Once
var instance *Singleton
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
sync.Pool
Пул временных объектов для снижения нагрузки на GC.
var pool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}
buf := pool.Get().([]byte)
// использование
pool.Put(buf)
Особенности:
- Объекты могут быть удалены GC в любой момент
- Не подходит для долгоживущих объектов
- Эффективен для часто создаваемых/удаляемых объектов
sync.Cond
Условная переменная для сигнализации между горутинами.
mu := sync.Mutex{}
cond := sync.NewCond(&mu)
// Ожидание
cond.L.Lock()
for !condition {
cond.Wait()
}
cond.L.Unlock()
// Сигнал
cond.Signal() // один ожидающий
cond.Broadcast() // все ожидающие
Важно: Wait() автоматически отпускает блокировку и блокирует горутину, после пробуждения снова захватывает блокировку.
sync.Map
Потокобезопасная map с оптимизациями для конкурентного доступа.
Методы: Load, Store, LoadOrStore, Delete, Range
Когда использовать:
- Частые чтения и редкие записи
- Разные горутины работают с разными ключами
- Нельзя использовать обычную map с мьютексом
Когда НЕ использовать:
- Есть типизированные ключи/значения
- Нужна производительность как у обычной map с мьютексом
- Частые записи в одни и те же ключи
Атомарные операции (sync/atomic)
Низкоуровневые операции без блокировок для простых типов.
var counter int64
atomic.AddInt64(&counter, 1)
value := atomic.LoadInt64(&counter)
atomic.StoreInt64(&counter, 100)
swapped := atomic.CompareAndSwapInt64(&counter, 100, 200)
Типы: int32, int64, uint32, uint64, uintptr, unsafe.Pointer
Практические рекомендации
- Каналы — предпочтительный способ синхронизации в Go (share memory by communicating)
- Мьютексы — когда нужно защитить сложную структуру данных
- WaitGroup — для ожидания завершения группы задач
- Once — для ленивой инициализации
- Pool — для снижения аллокаций в высоконагруженных системах
- Atomic — для простых счетчиков и флагов
Типичные проблемы
- Deadlock — взаимная блокировка горутин
- Race condition — несинхронизированный доступ к данным
- Live lock — горутины активны, но не могут завершить работу
- Starvation — одна горутина не получает доступ к ресурсу
Для обнаружения используйте go run -race и статические анализаторы.