Перейти к содержанию

Конкурентность в Go

TL;DR

  • go f() — запустить горутину (~2 KB stack, миллионы возможны).
  • Канал — типизированная очередь между горутинами.
  • Unbuffered: send блокируется до receive.
  • sync.WaitGroup — ждать N горутин.
  • sync.Mutex / RWMutex — критическая секция.
  • context.Context — отмена и таймауты.
  • go run -race — детектор гонок, всегда включай в CI.

Горутины

go func() {
    fmt.Println("hi from goroutine")
}()

Стартуют через go, шедулятся runtime'ом Go (модель G/M/P). Стек растёт динамически, начинается с ~2 KB.

Каналы

ch := make(chan int)        // unbuffered
ch := make(chan int, 100)   // buffered, capacity 100

ch <- 1                     // send
v := <-ch                   // receive
v, ok := <-ch               // ok=false если канал закрыт И буфер пуст
close(ch)                   // закрыть
for v := range ch { ... }   // итерация до close

Правила: 1. Закрывает только sender. 2. Send в closed → panic. 3. Receive из closed → возвращает zero value моментально. 4. nil-канал в select никогда не срабатывает (полезно для динамического отключения case).

sync.WaitGroup

Ждём N горутин:

var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        // работа
    }(i)
}
wg.Wait()

sync.Mutex / RWMutex

Защищаем shared state:

var (
    mu      sync.Mutex
    counter int
)

mu.Lock()
counter++
mu.Unlock()

RWMutex для read-heavy: много RLock() параллельно, Lock() только один.

atomic

Для одной переменной — atomic дешевле Mutex'а:

var counter int64
atomic.AddInt64(&counter, 1)
v := atomic.LoadInt64(&counter)

context.Context

Отмена и таймауты:

ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()

select {
case result := <-doWork(ctx):
    return result
case <-ctx.Done():
    return nil, ctx.Err()
}

Конвенция: первый аргумент функции — ctx context.Context.

Worker pool

func worker(jobs <-chan Task, results chan<- Result) {
    for j := range jobs {
        results <- process(j)
    }
}

jobs := make(chan Task, 100)
results := make(chan Result, 100)
for w := 0; w < N; w++ {
    go worker(jobs, results)
}

for _, t := range tasks {
    jobs <- t
}
close(jobs)

Параллельный fetch с лимитом

Fan-out/fan-in с семафором:

sem := make(chan struct{}, 10)   // max 10 in-flight
var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    sem <- struct{}{}
    go func(u string) {
        defer wg.Done()
        defer func() { <-sem }()
        fetch(u)
    }(url)
}
wg.Wait()

Race condition и detector

go run -race ./...
go test -race ./...

Включай race detector в CI всегда (стоит ~2x памяти, ~10% CPU).

Deadlock

Простое правило: всегда захватывай несколько mutex'ов в одном порядке. Если нужен inverse — переписывай на каналы или actor-pattern.

📖 Связанные задачи: must-solve-100 → C-013..C-018, M-001..M-004.