Назад
Golang

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

Golang был создан с нуля для эпохи многоядерных процессоров и распределённых систем. Его слоган «Do not communicate by sharing memory; instead, share memory by communicating» идеально отражает философию языка

Паттерны конкурентности в Go не просто «возможны» - они встроены в язык. Это делает параллельный код в Go:

  • Простым и читаемым
  • Масштабируемым (десятки тысяч горутин без overhead’а)
  • Производительным (минимальные контекстные переключения)

Примеры паттернов

1. Producer-Consumer (через каналы)

Каналы - это по-сути безопасные очереди

package main

import (
    "fmt"
    "time"
)

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        fmt.Printf("Произвёл: %d\n", i)
        ch <- i
        time.Sleep(time.Millisecond * 300)
    }
    close(ch) // Не забудь закрыть:)
}

func consumer(ch <-chan int) {
    for val := range ch {
        fmt.Printf("Потребил: %d\n", val)
        time.Sleep(time.Millisecond * 500)
    }
}

func main() {
    ch := make(chan int, 3) // Буферизированный канал
    go producer(ch)
    go consumer(ch)
    
    time.Sleep(time.Second * 3)
    fmt.Println("Готово")
}

Канал автоматически синхронизирует доступ: отправка блокируется при полном буфере, получение - при пустом.

2. Worker Pool

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

package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Воркер %d обработал задачу %d\n", id, job)
        // Симулируем работу
        // time.Sleep(time.Second)
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3
    
    jobs := make(chan int, numJobs)
    var wg sync.WaitGroup
    
    // Запускаем воркеров
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, &wg)
    }
    
    // Отправляем задачи
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)
    
    wg.Wait()
    fmt.Println("Все задачи выполнены")
}

WaitGroup гарантирует, что main дождётся завершения всех воркеров.

3. Pipeline

Данные проходят через цепочку горутин, каждая выполняет свою стадию обработки.

package main

import "fmt"

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

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

func main() {
    in := generator(1, 2, 3, 4, 5)
    out := square(in)
    
    for result := range out {
        fmt.Println(result) // 1, 4, 9, 16, 25
    }
}

Легко масштабировать и добавлять этапы для параллельной обработки.

4. Проверяем ctx

Важно помнить про ctx, так как параллельные операции зачастую применяются в загруженных участках кода, и их лучше завершать при необходимости. Например, клиент дернул http-ручку, но решил отменить запрос. Нет смысла производить дальнейшую обработку. В таком случае нам нужно завершить выполнение всех горутин, и не тратить ресурсы впустую

Ну или если у нас есть определенный timeout для выполнения кода. Например, мы знаем, что обработка не может превышать час, то мы можем добавить context.WithTimeout, чтобы воркер просто завершился при неожиданно появившемся баге, и не простаивал бесконечно

package main

import (
    "context"
    "fmt"
    "time"
)

func longTask(ctx context.Context) error {
    select {
    case <-time.After(2 * time.Second):
        fmt.Println("Задача завершена")
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    
    if err := longTask(ctx); err != nil {
        fmt.Println("Задача отменена:", err)
    }
}

Инструменты для конкурентности в go мне очень нравятся. Они производительные, просто читаются и удобные в использовании. Но тут просто нужно быть осторожным, можно жестко запороть все из-за банальной ошибки, поэтому нужно смотреть в оба👀(Но опять же, это не проблема go, это просто зачастую связанно с большим кол-вом логики во всех этих обработках🫠)