Конкурентность в Go: channels vs sync — когда что использовать

Конкурентность в Go: channels vs sync — когда что использовать

Подробный разбор конкурентности в Go: когда использовать channels, а когда sync.Mutex. Сравнение производительности, паттерны worker pool, fan-out/fan-in, правильное использование WaitGroup, Once, Pool. Практические примеры и бенчмарки.

Работаешь с Go? Значит, рано или поздно столкнешься с конкурентностью. Go продвигает философию “Don’t communicate by sharing memory, share memory by communicating” — и все сразу бегут использовать channels везде. Но это не всегда правильно.

Недавно профилировал сервис на Go и обнаружил, что замена channels на mutex в критичных местах дала прирост производительности в 3 раза. Расскажу, когда что использовать, и почему слепое следование “Go way” может убить производительность.

Конкурентность vs Параллелизм

Сначала разберемся с терминами, потому что многие путают.

Конкурентность (Concurrency) — структура программы, которая позволяет выполнять несколько задач, переключаясь между ними. Может работать на одном ядре.

Параллелизм (Parallelism) — одновременное выполнение нескольких задач на разных ядрах.

// Конкурентность — 2 горутины на 1 ядре (переключаются)
runtime.GOMAXPROCS(1)
go task1()
go task2()

// Параллелизм — 2 горутины на 2 ядрах (работают одновременно)
runtime.GOMAXPROCS(2)
go task1()
go task2()

Go дает конкурентность из коробки через горутины. Параллелизм зависит от GOMAXPROCS (по умолчанию = количество ядер).

Channels: основы

Channel — это типизированный канал для передачи данных между горутинами. Основной примитив конкурентности в Go.

// Unbuffered channel — блокирует отправителя до получения
ch := make(chan int)

// Buffered channel — блокирует только когда буфер полон
ch := make(chan int, 10)

// Отправка
ch <- 42

// Получение
value := <-ch

// Закрытие
close(ch)

Unbuffered vs Buffered channels

Unbuffered channel — синхронизация. Отправитель блокируется, пока получатель не прочитает.

func main() {
    ch := make(chan int) // unbuffered
    
    go func() {
        fmt.Println("Sending...")
        ch <- 42 // Блокируется здесь
        fmt.Println("Sent!")
    }()
    
    time.Sleep(time.Second)
    fmt.Println("Receiving...")
    value := <-ch // Разблокирует отправителя
    fmt.Println("Received:", value)
}
// Output:
// Sending...
// (пауза 1 сек)
// Receiving...
// Sent!
// Received: 42

Buffered channel — асинхронность до заполнения буфера.

func main() {
    ch := make(chan int, 2) // буфер на 2 элемента
    
    ch <- 1 // Не блокируется
    ch <- 2 // Не блокируется
    // ch <- 3 // Заблокируется — буфер полон
    
    fmt.Println(<-ch) // 1
    fmt.Println(<-ch) // 2
}

Select — мультиплексирование каналов

select позволяет ждать на нескольких каналах одновременно:

func worker(done <-chan struct{}, tasks <-chan Task) {
    for {
        select {
        case <-done:
            fmt.Println("Worker stopping")
            return
        case task := <-tasks:
            process(task)
        }
    }
}

Non-blocking операции с default:

select {
case msg := <-ch:
    fmt.Println("Received:", msg)
default:
    fmt.Println("No message available")
}

Timeout:

select {
case result := <-ch:
    return result, nil
case <-time.After(5 * time.Second):
    return nil, errors.New("timeout")
}

sync пакет: основы

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

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

Защищает shared state от одновременного доступа:

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

sync.RWMutex — читатели и писатели

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

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock() // Несколько читателей могут работать одновременно
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock() // Только один писатель
    defer c.mu.Unlock()
    c.data[key] = value
}

sync.WaitGroup — ожидание завершения

Ждем завершения группы горутин:

func processItems(items []Item) {
    var wg sync.WaitGroup
    
    for _, item := range items {
        wg.Add(1)
        go func(item Item) {
            defer wg.Done()
            process(item)
        }(item)
    }
    
    wg.Wait() // Ждем всех
}

Важно: wg.Add() вызывается ДО запуска горутины, не внутри!

// НЕПРАВИЛЬНО — race condition
for _, item := range items {
    go func(item Item) {
        wg.Add(1) // Может не успеть выполниться до wg.Wait()
        defer wg.Done()
        process(item)
    }(item)
}
wg.Wait()

// ПРАВИЛЬНО
for _, item := range items {
    wg.Add(1)
    go func(item Item) {
        defer wg.Done()
        process(item)
    }(item)
}
wg.Wait()

sync.Once — выполнить один раз

Гарантирует однократное выполнение (инициализация, singleton):

type Database struct {
    once sync.Once
    conn *sql.DB
}

func (d *Database) Connection() *sql.DB {
    d.once.Do(func() {
        // Выполнится только один раз, даже при конкурентных вызовах
        d.conn, _ = sql.Open("postgres", "...")
    })
    return d.conn
}

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

Уменьшает давление на GC для часто создаваемых объектов:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process(data []byte) {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()
    
    buf.Write(data)
    // работаем с buf
}

sync.Map — конкурентная map

Когда ключи стабильны или читателей много больше писателей:

var cache sync.Map

// Запись
cache.Store("key", "value")

// Чтение
if val, ok := cache.Load("key"); ok {
    fmt.Println(val.(string))
}

// LoadOrStore — атомарно
actual, loaded := cache.LoadOrStore("key", "default")

// Удаление
cache.Delete("key")

// Итерация
cache.Range(func(key, value interface{}) bool {
    fmt.Println(key, value)
    return true // продолжить итерацию
})

Важно: sync.Map НЕ всегда быстрее map + RWMutex. Используй когда:

  • Ключи стабильны (редко добавляются новые)
  • Много читателей, мало писателей
  • Горутины работают с разными ключами

sync.Cond — условные переменные

Для сложной координации (редко нужен):

type Queue struct {
    mu    sync.Mutex
    cond  *sync.Cond
    items []int
}

func NewQueue() *Queue {
    q := &Queue{}
    q.cond = sync.NewCond(&q.mu)
    return q
}

func (q *Queue) Push(item int) {
    q.mu.Lock()
    q.items = append(q.items, item)
    q.cond.Signal() // Разбудить одного ожидающего
    q.mu.Unlock()
}

func (q *Queue) Pop() int {
    q.mu.Lock()
    for len(q.items) == 0 {
        q.cond.Wait() // Ждать сигнала
    }
    item := q.items[0]
    q.items = q.items[1:]
    q.mu.Unlock()
    return item
}

Когда использовать channels

Channels хороши для:

1. Передача ownership данных

Когда одна горутина “передает” данные другой и больше не использует их:

func producer(out chan<- Task) {
    for {
        task := createTask()
        out <- task // Передаем ownership
        // Больше не трогаем task
    }
}

func consumer(in <-chan Task) {
    for task := range in {
        process(task) // Теперь мы владеем task
    }
}

2. Координация и сигнализация

Graceful shutdown, отмена операций:

func server(done <-chan struct{}) {
    for {
        select {
        case <-done:
            fmt.Println("Shutting down...")
            return
        default:
            handleRequest()
        }
    }
}

func main() {
    done := make(chan struct{})
    go server(done)
    
    // ... работаем ...
    
    close(done) // Сигнал на завершение
}

3. Pipeline паттерн

Цепочка обработки данных:

func gen(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func sq(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // Pipeline: gen -> sq -> print
    for n := range sq(gen(1, 2, 3, 4)) {
        fmt.Println(n) // 1, 4, 9, 16
    }
}

4. Fan-out / Fan-in

Распределение работы и сбор результатов:

// Fan-out: один источник, много обработчиков
func fanOut(in <-chan Task, workers int) []<-chan Result {
    outs := make([]<-chan Result, workers)
    for i := 0; i < workers; i++ {
        outs[i] = worker(in)
    }
    return outs
}

// Fan-in: много источников, один получатель
func fanIn(channels ...<-chan Result) <-chan Result {
    var wg sync.WaitGroup
    out := make(chan Result)
    
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan Result) {
            defer wg.Done()
            for v := range c {
                out <- v
            }
        }(ch)
    }
    
    go func() {
        wg.Wait()
        close(out)
    }()
    
    return out
}

5. Rate limiting

Ограничение скорости обработки:

func rateLimited(requests <-chan Request) {
    // 10 запросов в секунду
    limiter := time.NewTicker(100 * time.Millisecond)
    defer limiter.Stop()
    
    for req := range requests {
        <-limiter.C // Ждем тик
        go handle(req)
    }
}

// Burst rate limiting
func burstRateLimited(requests <-chan Request) {
    // Разрешаем burst до 3, потом 1 в секунду
    burstyLimiter := make(chan time.Time, 3)
    
    // Заполняем burst
    for i := 0; i < 3; i++ {
        burstyLimiter <- time.Now()
    }
    
    // Пополняем со скоростью 1/сек
    go func() {
        for t := range time.Tick(time.Second) {
            burstyLimiter <- t
        }
    }()
    
    for req := range requests {
        <-burstyLimiter
        go handle(req)
    }
}

Когда использовать sync

Mutex и другие примитивы sync лучше для:

1. Защита shared state

Когда нужно просто защитить переменную:

// С mutex — просто и быстро
type Stats struct {
    mu       sync.Mutex
    requests int
    errors   int
}

func (s *Stats) RecordRequest() {
    s.mu.Lock()
    s.requests++
    s.mu.Unlock()
}

func (s *Stats) RecordError() {
    s.mu.Lock()
    s.errors++
    s.mu.Unlock()
}

// С channels — сложнее и медленнее
type Stats struct {
    requests chan int
    errors   chan int
    data     struct {
        requests int
        errors   int
    }
}

func NewStats() *Stats {
    s := &Stats{
        requests: make(chan int),
        errors:   make(chan int),
    }
    go s.run()
    return s
}

func (s *Stats) run() {
    for {
        select {
        case <-s.requests:
            s.data.requests++
        case <-s.errors:
            s.data.errors++
        }
    }
}

2. Read-heavy workloads

Когда читателей много, писателей мало — RWMutex:

type ConfigStore struct {
    mu     sync.RWMutex
    config Config
}

func (c *ConfigStore) Get() Config {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.config
}

func (c *ConfigStore) Update(cfg Config) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.config = cfg
}

3. Инициализация (sync.Once)

Ленивая инициализация singleton:

var (
    instance *Database
    once     sync.Once
)

func GetDatabase() *Database {
    once.Do(func() {
        instance = &Database{}
        instance.connect()
    })
    return instance
}

4. Переиспользование объектов (sync.Pool)

Уменьшение аллокаций:

var jsonPool = sync.Pool{
    New: func() interface{} {
        return &bytes.Buffer{}
    },
}

func MarshalJSON(v interface{}) ([]byte, error) {
    buf := jsonPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        jsonPool.Put(buf)
    }()
    
    enc := json.NewEncoder(buf)
    if err := enc.Encode(v); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

Бенчмарки: channels vs mutex

Давай измерим производительность. Задача: инкрементировать счетчик из нескольких горутин.

// benchmark_test.go
package main

import (
    "sync"
    "sync/atomic"
    "testing"
)

// Вариант 1: Mutex
type MutexCounter struct {
    mu    sync.Mutex
    value int64
}

func (c *MutexCounter) Inc() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

// Вариант 2: Channel
type ChannelCounter struct {
    inc   chan struct{}
    value int64
}

func NewChannelCounter() *ChannelCounter {
    c := &ChannelCounter{
        inc: make(chan struct{}),
    }
    go c.run()
    return c
}

func (c *ChannelCounter) run() {
    for range c.inc {
        c.value++
    }
}

func (c *ChannelCounter) Inc() {
    c.inc <- struct{}{}
}

// Вариант 3: Atomic
type AtomicCounter struct {
    value int64
}

func (c *AtomicCounter) Inc() {
    atomic.AddInt64(&c.value, 1)
}

// Бенчмарки
func BenchmarkMutexCounter(b *testing.B) {
    c := &MutexCounter{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            c.Inc()
        }
    })
}

func BenchmarkChannelCounter(b *testing.B) {
    c := NewChannelCounter()
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            c.Inc()
        }
    })
}

func BenchmarkAtomicCounter(b *testing.B) {
    c := &AtomicCounter{}
    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            c.Inc()
        }
    })
}

Результаты (MacBook Pro M1, 8 cores):

BenchmarkMutexCounter-8      50000000    24.5 ns/op
BenchmarkChannelCounter-8    10000000   145.0 ns/op
BenchmarkAtomicCounter-8    200000000     6.2 ns/op
Подходns/opОтносительно
Atomic6.21x (эталон)
Mutex24.54x медленнее
Channel145.023x медленнее

Вывод: Для простых операций над shared state mutex в 6 раз быстрее channels. Atomic еще в 4 раза быстрее mutex.

Когда channels быстрее?

Channels выигрывают при сложной координации, когда альтернатива — много mutex и условных переменных:

// Pipeline с channels — просто и эффективно
func pipeline(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for v := range in {
            out <- v * 2
        }
        close(out)
    }()
    return out
}

// То же самое на mutex — ад
type PipelineStage struct {
    mu     sync.Mutex
    cond   *sync.Cond
    input  []int
    output []int
    closed bool
}
// ... 50+ строк кода ...

Паттерны конкурентности

Worker Pool

Ограниченное число воркеров обрабатывают задачи из очереди:

func workerPool(numWorkers int, tasks <-chan Task, results chan<- Result) {
    var wg sync.WaitGroup
    
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            for task := range tasks {
                result := process(task)
                results <- result
            }
        }(i)
    }
    
    wg.Wait()
    close(results)
}

func main() {
    tasks := make(chan Task, 100)
    results := make(chan Result, 100)
    
    // Запускаем 10 воркеров
    go workerPool(10, tasks, results)
    
    // Отправляем задачи
    go func() {
        for _, t := range getAllTasks() {
            tasks <- t
        }
        close(tasks)
    }()
    
    // Собираем результаты
    for result := range results {
        handleResult(result)
    }
}

Semaphore (ограничение конкурентности)

Ограничиваем количество одновременных операций:

// Semaphore через buffered channel
type Semaphore chan struct{}

func NewSemaphore(n int) Semaphore {
    return make(chan struct{}, n)
}

func (s Semaphore) Acquire() {
    s <- struct{}{}
}

func (s Semaphore) Release() {
    <-s
}

// Использование
func processWithLimit(items []Item, maxConcurrency int) {
    sem := NewSemaphore(maxConcurrency)
    var wg sync.WaitGroup
    
    for _, item := range items {
        wg.Add(1)
        sem.Acquire()
        
        go func(item Item) {
            defer wg.Done()
            defer sem.Release()
            
            process(item)
        }(item)
    }
    
    wg.Wait()
}

Context для отмены

Используй context для graceful cancellation:

func worker(ctx context.Context, tasks <-chan Task) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case task, ok := <-tasks:
            if !ok {
                return nil // Канал закрыт
            }
            if err := process(ctx, task); err != nil {
                return err
            }
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    tasks := make(chan Task)
    
    // Запускаем воркеры
    g, ctx := errgroup.WithContext(ctx)
    for i := 0; i < 10; i++ {
        g.Go(func() error {
            return worker(ctx, tasks)
        })
    }
    
    // Отправляем задачи
    go func() {
        defer close(tasks)
        for _, t := range getAllTasks() {
            select {
            case <-ctx.Done():
                return
            case tasks <- t:
            }
        }
    }()
    
    if err := g.Wait(); err != nil {
        log.Fatal(err)
    }
}

errgroup — координация с ошибками

Пакет golang.org/x/sync/errgroup упрощает работу с группой горутин:

import "golang.org/x/sync/errgroup"

func fetchAll(ctx context.Context, urls []string) ([]Response, error) {
    g, ctx := errgroup.WithContext(ctx)
    results := make([]Response, len(urls))
    
    for i, url := range urls {
        i, url := i, url // Capture
        g.Go(func() error {
            resp, err := fetch(ctx, url)
            if err != nil {
                return err
            }
            results[i] = resp
            return nil
        })
    }
    
    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

Singleflight — дедупликация запросов

Предотвращаем thundering herd (много одинаковых запросов):

import "golang.org/x/sync/singleflight"

var requestGroup singleflight.Group

func getData(key string) ([]byte, error) {
    // Все конкурентные запросы с одним key выполнят только один fetch
    v, err, _ := requestGroup.Do(key, func() (interface{}, error) {
        return fetchFromDB(key)
    })
    if err != nil {
        return nil, err
    }
    return v.([]byte), nil
}

Очень полезно для кеша: если 1000 запросов одновременно просят один ключ, в БД пойдет только 1 запрос.

Race Conditions и как их избежать

Детектор рейсов

Go имеет встроенный детектор race conditions:

go test -race ./...
go build -race ./cmd/server

Пример race condition:

// RACE CONDITION
func main() {
    counter := 0
    
    for i := 0; i < 1000; i++ {
        go func() {
            counter++ // Несинхронизированный доступ
        }()
    }
    
    time.Sleep(time.Second)
    fmt.Println(counter) // Неопределенный результат
}
$ go run -race main.go
==================
WARNING: DATA RACE
Read at 0x00c0000a4010 by goroutine 8:
  main.main.func1()
      /main.go:11 +0x3c

Previous write at 0x00c0000a4010 by goroutine 7:
  main.main.func1()
      /main.go:11 +0x52
==================

Исправление с mutex

func main() {
    var mu sync.Mutex
    counter := 0
    
    for i := 0; i < 1000; i++ {
        go func() {
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }
    
    time.Sleep(time.Second)
    fmt.Println(counter) // 1000
}

Исправление с atomic

func main() {
    var counter int64
    
    for i := 0; i < 1000; i++ {
        go func() {
            atomic.AddInt64(&counter, 1)
        }()
    }
    
    time.Sleep(time.Second)
    fmt.Println(counter) // 1000
}

Исправление с channel

func main() {
    counter := 0
    inc := make(chan struct{}, 1000)
    
    for i := 0; i < 1000; i++ {
        go func() {
            inc <- struct{}{}
        }()
    }
    
    for i := 0; i < 1000; i++ {
        <-inc
        counter++
    }
    
    fmt.Println(counter) // 1000
}

Распространенные ошибки

1. Goroutine leak

// УТЕЧКА — горутина никогда не завершится
func leaky() {
    ch := make(chan int)
    go func() {
        val := <-ch // Блокируется навсегда
        fmt.Println(val)
    }()
    // ch никогда не получит значение и не закроется
}

// ИСПРАВЛЕНИЕ — используй context для отмены
func notLeaky(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-ctx.Done():
            return
        }
    }()
}

2. Закрытие канала несколько раз

// ПАНИКА
ch := make(chan int)
close(ch)
close(ch) // panic: close of closed channel

// ИСПРАВЛЕНИЕ — закрывай только один раз
type SafeChannel struct {
    ch     chan int
    once   sync.Once
    closed bool
}

func (s *SafeChannel) Close() {
    s.once.Do(func() {
        close(s.ch)
        s.closed = true
    })
}

3. Отправка в закрытый канал

// ПАНИКА
ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel

// Закрывать должен только отправитель!
func producer(ch chan<- int) {
    defer close(ch) // Закрываем когда закончили отправлять
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

4. Копирование mutex

// НЕПРАВИЛЬНО — mutex копируется
type Counter struct {
    mu    sync.Mutex
    value int
}

func (c Counter) Inc() { // Получатель по значению — копирует mutex!
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

// ПРАВИЛЬНО — получатель по указателю
func (c *Counter) Inc() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

5. WaitGroup внутри горутины

// НЕПРАВИЛЬНО — race condition
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    go func() {
        wg.Add(1) // Может не выполниться до wg.Wait()
        defer wg.Done()
        work()
    }()
}
wg.Wait()

// ПРАВИЛЬНО — Add до запуска горутины
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        work()
    }()
}
wg.Wait()

6. Захват переменной цикла

// НЕПРАВИЛЬНО — все горутины получат последнее значение i
for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(i) // Все напечатают 10
    }()
}

// ПРАВИЛЬНО — явная передача значения
for i := 0; i < 10; i++ {
    go func(i int) {
        fmt.Println(i) // 0, 1, 2, ..., 9 (в случайном порядке)
    }(i)
}

// Или локальная переменная
for i := 0; i < 10; i++ {
    i := i // Shadow
    go func() {
        fmt.Println(i)
    }()
}

Примечание: В Go 1.22+ эта проблема исправлена — каждая итерация создает новую переменную.

Таблица: когда что использовать

ЗадачаРешениеПочему
Защита переменнойsync.MutexПросто и быстро
Много читателей, мало писателейsync.RWMutexПараллельное чтение
СчетчикatomicСамый быстрый
Однократная инициализацияsync.OnceПотокобезопасный singleton
Передача данных между горутинамиchannelOwnership transfer
Graceful shutdownchannel или contextСигнализация
Pipeline обработкиchannelЕстественный паттерн
Worker poolchannel + sync.WaitGroupКоординация воркеров
Ограничение конкурентностиBuffered channel (semaphore)Простой semaphore
Переиспользование объектовsync.PoolУменьшение аллокаций
Конкурентная mapsync.Map или map + RWMutexЗависит от паттерна доступа
Дедупликация запросовsingleflightПредотвращение thundering herd
Группа горутин с ошибкамиerrgroupУдобная координация

Best Practices

1. Начинай с простого

// Сначала mutex — просто и понятно
type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

// Переходи на channels только если нужна сложная координация

2. Используй -race в тестах и CI

# .github/workflows/test.yml
- name: Test with race detector
  run: go test -race -v ./...

3. Документируй правила синхронизации

// Counter is safe for concurrent use.
// All methods may be called from multiple goroutines.
type Counter struct {
    mu    sync.Mutex
    value int
}

4. Держи критическую секцию короткой

// ПЛОХО — долгая операция под локом
func (c *Cache) Process(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    
    data := c.data[key]
    result := expensiveOperation(data) // Долго!
    c.data[key] = result
}

// ХОРОШО — лок только для доступа к данным
func (c *Cache) Process(key string) {
    c.mu.RLock()
    data := c.data[key]
    c.mu.RUnlock()
    
    result := expensiveOperation(data) // Вне лока
    
    c.mu.Lock()
    c.data[key] = result
    c.mu.Unlock()
}

5. Используй context для отмены

func worker(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            if err := doWork(ctx); err != nil {
                return err
            }
        }
    }
}

6. Закрывай каналы на стороне отправителя

func producer(out chan<- int) {
    defer close(out) // Отправитель закрывает
    for i := 0; i < 10; i++ {
        out <- i
    }
}

func consumer(in <-chan int) {
    for v := range in { // Получатель итерирует до закрытия
        fmt.Println(v)
    }
}

Заключение

Конкурентность в Go — мощный инструмент, но важно выбирать правильный примитив:

Используй channels когда:

  • Передаешь ownership данных
  • Нужна координация и сигнализация
  • Строишь pipeline
  • Реализуешь graceful shutdown

Используй sync когда:

  • Защищаешь shared state
  • Нужна максимальная производительность
  • Много читателей, мало писателей
  • Инициализируешь singleton

Правило большого пальца:

Если сомневаешься — начни с mutex. Переходи на channels только когда код становится сложнее, чем мог бы быть с channels.

Помни: “Don’t communicate by sharing memory” — это guideline, а не закон. Иногда mutex — правильный выбор.

Если хочешь глубже разобраться в Go, почитай статью про антипаттерны в Go коде — там реальные примеры плохого кода из production. А для работы с микросервисами пригодится статья про gRPC в Go .


P.S. Всегда запускай тесты с -race. Лучше найти race condition в CI, чем в production в 3 часа ночи.