Продвинутые паттерны проектирования в 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)
}
Почему это круто:
- Нет необходимости в сложных иерархиях конструкторов.
- Легко добавлять новые опции без изменения существующего кода.
- Код остается читаемым даже при десятках параметров.