Паттерны проектирования в Go на продвинутом уровне с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Паттерны проектирования в Go: продвинутый уровень для профессиональных разработчиков

Go (Golang) — язык, созданный для эффективной работы с конкурентностью и микросервисной архитектурой. Базовые паттерны проектирования, такие как Singleton или Factory, хорошо известны, но для создания высоконагруженных и отказоустойчивых систем требуется продвинутый уровень владения паттернами. В этой статье мы разберем четыре ключевых паттерна: Fan-Out/Fan-In, Pipeline, Worker Pool и Circuit Breaker. Каждый из них решает практические задачи масштабирования, параллельной обработки и защиты системы от каскадных сбоев.

1. Паттерн Fan-Out / Fan-In: параллельная обработка данных

Этот паттерн — основа параллельных вычислений в Go. Fan-Out (веерный выход) распределяет нагрузку между несколькими горутинами, а Fan-In (веерный вход) собирает результаты обратно в один канал.

Реализация Fan-Out

Представим задачу: нужно обработать 100 URL-адресов. Вместо последовательной обработки мы запускаем 10 горутин, каждая из которых берет задачу из общего канала.

package main

import (
    "fmt"
    "sync"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        // Имитация обработки
        results <- job * 2
        fmt.Printf("Worker %d processed job %d\n", id, job)
    }
}

func main() {
    const numJobs = 20
    const numWorkers = 5

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup

    // Fan-Out: запускаем воркеров
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Отправляем задачи
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Fan-In: ждем завершения и закрываем results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Собираем результаты
    for result := range results {
        fmt.Println("Result:", result)
    }
}

Ключевые моменты:

  • Канал jobs служит точкой Fan-Out: все воркеры читают из него.
  • Канал results — точка Fan-In: все воркеры пишут в него.
  • Использование sync.WaitGroup гарантирует, что мы закроем results только после завершения всех горутин.

2. Паттерн Pipeline: конвейерная обработка данных

Pipeline — это цепочка стадий, где каждая стадия обрабатывает данные и передает их следующей. В Go это реализуется через последовательность каналов. Паттерн идеально подходит для ETL-процессов, обработки логов или трансформации данных.

Пример: конвейер обработки текста

package main

import "fmt"

// Стадия 1: генерация чисел
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// Стадия 2: возведение в квадрат
func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

// Стадия 3: умножение на 2
func multiply(in <-chan int, factor int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * factor
        }
        close(out)
    }()
    return out
}

func main() {
    // Сборка пайплайна
    pipeline := multiply(square(generate(1, 2, 3, 4)), 10)

    // Потребление результатов
    for result := range pipeline {
        fmt.Println(result) // 10, 40, 90, 160
    }
}

Преимущества:

  • Каждая стадия — отдельная горутина, работающая конкурентно.
  • Легко добавлять новые стадии (например, фильтрацию или агрегацию).
  • Управление потоком данных через буферизированные каналы.

3. Паттерн Worker Pool: управление пулом горутин

Worker Pool — это более структурированная версия Fan-Out/Fan-In. Вместо динамического запуска горутин мы создаем фиксированный пул воркеров, которые живут всё время выполнения программы. Это снижает накладные расходы на создание/уничтожение горутин.

Реализация семафора на каналах

package main

import (
    "fmt"
    "time"
)

type Pool struct {
    sem chan struct{}
    jobs chan func()
}

func NewPool(size int) *Pool {
    return &Pool{
        sem:  make(chan struct{}, size),
        jobs: make(chan func(), 100),
    }
}

func (p *Pool) Submit(task func()) {
    p.jobs <- task
}

func (p *Pool) Start() {
    for task := range p.jobs {
        // Семафор: блокируем, если пул полон
        p.sem <- struct{}{}
        go func(t func()) {
            defer func() { <-p.sem }() // освобождаем слот
            t()
        }(task)
    }
}

func main() {
    pool := NewPool(3) // максимум 3 одновременных задачи
    go pool.Start()

    for i := 0; i < 10; i++ {
        id := i
        pool.Submit(func() {
            fmt.Printf("Task %d started\n", id)
            time.Sleep(100 * time.Millisecond)
            fmt.Printf("Task %d done\n", id)
        })
    }

    time.Sleep(2 * time.Second)
}

Почему это важно:

  • Контроль над количеством одновременно выполняемых горутин.
  • Защита от перегрузки системы (ограничение по памяти/соединениям).
  • Переиспользование горутин снижает нагрузку на GC.

4. Паттерн Circuit Breaker: защита от каскадных сбоев

Circuit Breaker — критически важный паттерн для микросервисной архитектуры. Он предотвращает цепную реакцию сбоев, когда один медленный/

Блоги

Рекомендаций по книгам