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

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

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

Начать курс

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

Go (Golang) — это язык, который поощряет простоту и читаемость кода. Однако, когда проект разрастается, даже самый лаконичный код может превратиться в хаос без правильной архитектуры. На продвинутом уровне разработчики Go активно используют паттерны проектирования, адаптированные под особенности языка: горутины, каналы, интерфейсы и дженерики (с Go 1.18+).

В этой статье мы разберем 4 мощных паттерна, которые помогут вам писать масштабируемый, тестируемый и производительный код на Go. Каждый паттерн сопровождается практическим примером.

1. Паттерн Pipeline (Конвейер) для потоковой обработки данных

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

Когда применять: Обработка логов, трансформация данных, ETL-процессы, потоковое видео.

Пример: Конвейер для фильтрации и трансформации чисел.

package main

import "fmt"

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

// Stage 2: Умножение на 2
func multiply(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * 2
        }
        close(out)
    }()
    return out
}

// Stage 3: Фильтрация (только четные)
func filterEven(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            if n%2 == 0 {
                out <- n
            }
        }
        close(out)
    }()
    return out
}

func main() {
    // Собираем конвейер
    pipeline := filterEven(multiply(generate(1, 2, 3, 4, 5)))

    for result := range pipeline {
        fmt.Println(result) // Вывод: 4, 8 (только четные после умножения)
    }
}

Плюсы: Естественная параллельная обработка, низкое потребление памяти (данные не накапливаются), легко добавлять новые этапы.

2. Fan-Out / Fan-In: масштабирование обработки

Этот паттерн — продвинутая эволюция Pipeline. Fan-Out (разветвление) запускает несколько горутин для параллельной обработки данных из одного канала. Fan-In (слияние) собирает результаты из нескольких каналов в один.

Когда применять: Парсинг веб-страниц, обработка большого количества файлов, параллельные запросы к API.

Пример: Параллельная проверка URL на доступность.

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func worker(urls <-chan string, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for url := range urls {
        resp, err := http.Get(url)
        if err != nil {
            results <- fmt.Sprintf("Ошибка: %s - %v", url, err)
            continue
        }
        resp.Body.Close()
        results <- fmt.Sprintf("OK: %s - %d", url, resp.StatusCode)
    }
}

func fanOutFanIn(urls []string, numWorkers int) []string {
    urlChan := make(chan string, len(urls))
    resultChan := make(chan string, len(urls))
    var wg sync.WaitGroup

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

    // Отправляем URL
    for _, url := range urls {
        urlChan <- url
    }
    close(urlChan)

    // Ожидаем завершения всех воркеров
    wg.Wait()
    close(resultChan)

    // Fan-In: собираем результаты
    var results []string
    for res := range resultChan {
        results = append(results, res)
    }
    return results
}

func main() {
    urls := []string{
        "https://google.com",
        "https://github.com",
        "https://nonexistent.example.com",
    }

    results := fanOutFanIn(urls, 3)
    for _, r := range results {
        fmt.Println(r)
    }
}

Важно: Количество воркеров должно быть оптимизировано под нагрузку. Слишком много горутин может привести к деградации производительности из-за переключения контекста.

3. Functional Options: гибкая конфигурация без конструкторов

В Go нет перегрузки методов или конструкторов с разными сигнатурами. Паттерн Functional Options решает проблему гибкой настройки объектов, сохраняя обратную совместимость API.

Когда применять: Настройка HTTP-клиентов, подключение к БД, конфигурация логгеров.

Пример: Гибкая настройка HTTP-сервера.

package main

import (
    "fmt"
    "time"
)

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

// Option — функциональный тип для опций
type Option func(*Server)

// WithTimeout — опция для таймаута
func WithTimeout(t time.Duration) Option {
    return func(s *Server) {
        s.timeout = t
    }
}

// WithMaxConn — опция для макс. соединений
func WithMaxConn(n int) Option {
    return func(s *Server) {
        s.maxConn = n
    }
}

// NewServer — конструктор с опциями
func NewServer(host string, port int, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    port,
        timeout: 30 * time.Second, // дефолтное значение
        maxConn: 100,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func main() {
    // Создаем сервер с кастомными настройками
    srv := NewServer("localhost", 8080,
        WithTimeout(5*time.Second),
        WithMaxConn(500),
    )

    fmt.Printf("Сервер: %s:%d, timeout=%v, maxConn=%d\n",
        srv.host, srv.port, srv.timeout, srv.maxConn)
}

Почему это круто: