Паттерны проектирования в 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 — критически важный паттерн для микросервисной архитектуры. Он предотвращает цепную реакцию сбоев, когда один медленный/