Модуль
1Горутины: основы2Каналы3select и таймауты4sync: Mutex, Once, atomic← вы здесь
Урок 4~12 минут

sync: Mutex, Once, atomic

Гонка данных: когда каналов недостаточно

Каналы — идеальны для передачи данных. Но иногда нескольким горутинам нужен общий доступ к одному значению. Без синхронизации возникает гонка данных (data race):

go
var counter int
 
func increment() {
    counter++  // НЕ атомарно: read → add → write
}
 
func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }
    wg.Wait()
    fmt.Println(counter)  // не 1000! гонка данных
}

Проверить наличие гонок: go run -race main.go. Детектор гонок найдёт проблему и покажет стек вызовов.


sync.Mutex: взаимное исключение

Mutex гарантирует, что в критической секции находится только одна горутина:

go
type SafeCounter struct {
    mu sync.Mutex
    value int
}
 
func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()  // всегда через defer!
    c.value++
}
 
func (c *SafeCounter) Get() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}
 
func main() {
    c := &SafeCounter{}
    var wg sync.WaitGroup
 
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Increment()
        }()
    }
    wg.Wait()
    fmt.Println(c.Get())  // всегда 1000
}

defer mu.Unlock() — обязательная идиома. Без defer легко забыть Unlock при панике или раннем return.

Правила Mutex:

  • Не копировать после первого использования (передавай по указателю)
  • Не блокировать рекурсивно — дедлок (нет reentrant mutex в Go)
  • Держать блокировку как можно меньше времени

sync.RWMutex: много читателей, один писатель

Когда чтений намного больше чем записей — RWMutex эффективнее:

go
type Cache struct {
    mu    sync.RWMutex
    store map[string]string
}
 
func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()          // разделяемая блокировка — несколько горутин могут читать
    defer c.mu.RUnlock()
    val, ok := c.store[key]
    return val, ok
}
 
func (c *Cache) Set(key, val string) {
    c.mu.Lock()           // эксклюзивная блокировка — только одна горутина
    defer c.mu.Unlock()
    c.store[key] = val
}

Когда использовать RWMutex вместо Mutex:

  • Читают намного чаще, чем пишут
  • Операции чтения занимают заметное время

Если записей столько же сколько чтений — RWMutex не даёт выигрыша (и чуть медленнее из-за overhead).


sync.Once: гарантированная однократная инициализация

Once.Do(f) выполняет f ровно один раз, даже при конкурентных вызовах:

go
type DB struct {
    once sync.Once
    conn *sql.DB
}
 
func (d *DB) getConn() *sql.DB {
    d.once.Do(func() {
        var err error
        d.conn, err = sql.Open("postgres", dsn)
        if err != nil {
            panic(err)
        }
    })
    return d.conn
}

Классический singleton:

go
var (
    instance *Config
    once     sync.Once
)
 
func GetConfig() *Config {
    once.Do(func() {
        instance = loadConfig()  // выполнится один раз
    })
    return instance
}

sync.Once безопаснее init() — инициализация откладывается до первого использования (lazy).


sync.Map: конкурентный map

Встроенный map не потокобезопасен. Для конкурентного доступа есть sync.Map:

go
var m sync.Map
 
// Запись
m.Store("key", "value")
 
// Чтение
val, ok := m.Load("key")
 
// Атомарное «загрузить или сохранить»
actual, loaded := m.LoadOrStore("key", "default")
 
// Удаление
m.Delete("key")
 
// Итерация
m.Range(func(k, v any) bool {
    fmt.Println(k, v)
    return true  // false — прервать итерацию
})

sync.Map оптимизирован для двух сценариев:

  1. Ключ пишется один раз, читается много раз
  2. Разные горутины работают с разными ключами (без пересечений)

Для других случаев обычный map + sync.Mutex часто быстрее.


sync/atomic: без блокировок

sync/atomic — атомарные операции на уровне CPU-инструкций. Быстрее Mutex для одиночных переменных:

go
import "sync/atomic"
 
var counter int64
 
// Атомарный инкремент
atomic.AddInt64(&counter, 1)
 
// Атомарное чтение (безопасно без mutex)
val := atomic.LoadInt64(&counter)
 
// Атомарная запись
atomic.StoreInt64(&counter, 0)
 
// Compare-and-swap
old := int64(5)
new := int64(10)
swapped := atomic.CompareAndSwapInt64(&counter, old, new)

Пример: быстрый счётчик запросов:

go
type Server struct {
    requests atomic.Int64  // Go 1.19+: типизированный атомик
}
 
func (s *Server) Handle() {
    s.requests.Add(1)
    // обработка...
}
 
func (s *Server) Stats() int64 {
    return s.requests.Load()
}

atomic.Int64, atomic.Bool, atomic.Pointer[T] — новые типизированные атомики из Go 1.19.


sync.Pool: переиспользование объектов

sync.Pool — пул объектов для снижения нагрузки на GC:

go
var bufPool = sync.Pool{
    New: func() any {
        return &bytes.Buffer{}
    },
}
 
func process(data string) string {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufPool.Put(buf)
    }()
 
    buf.WriteString(data)
    buf.WriteString(" processed")
    return buf.String()
}

Важно: объекты в Pool могут быть удалены GC в любой момент. Не храни в пуле объекты, которые нельзя пересоздать. Используй для буферов, временных структур — не для соединений с БД.


Выбор инструмента

ЗадачаИнструмент
Передача данных между горутинамиchan
Защита сложного состоянияsync.Mutex
Много чтений, мало записейsync.RWMutex
Однократная инициализацияsync.Once
Конкурентный mapsync.Map
Счётчик, флаг (одна переменная)sync/atomic
Переиспользование объектовsync.Pool
Ожидание группы горутинsync.WaitGroup
Отмена и дедлайныcontext
Mutex защищает критическую секцию — всегда defer mu.Unlock() сразу после Lock(). sync.Once гарантирует однократное выполнение. atomic быстрее Mutex для одиночных переменных.
Inside: 0
Waiting: 0
Counter: 0
Critical Section
UNLOCKED
goroutine G1
IDLE
goroutine G2
IDLE
goroutine G3
IDLE
goroutine G4
IDLE
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock()
// только одна горутина здесь
counter++
}
🎯
Миссия 1 из 4
Что случится если горутина вызовет Lock() на уже заблокированном Mutex?