Архитектурные паттерны в Go: как не утонуть в горутинах

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

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

Начать курс

Введение: Почему архитектура — это не роскошь, а необходимость

Когда вы только начинаете писать на Go, кажется, что всё просто: горутины летают, каналы гудят, а компилятор радует скоростью. Но проходит месяц-другой, и ваш pet-проект превращается в монстра с package main на 3000 строк и молитвами «лишь бы не упало». Знакомо?

Go даёт мощные инструменты, но не навязывает жёстких рамок. Это и плюс, и минус. Без чёткой архитектуры вы быстро запутаетесь в собственных горутинах. Именно здесь на помощь приходят архитектурные паттерны в Go — проверенные решения типовых проблем проектирования.

В этой статье мы разберём четыре ключевых паттерна, которые реально упростят жизнь: Repository, Service Layer, Worker Pool и Pipeline. Вы увидите примеры кода, поймёте, когда какой паттерн применять, и сможете сразу внедрить их в свои проекты. Поехали!

1. Repository Pattern: отделяем бизнес-логику от базы данных

Первый паттерн, который должен освоить каждый Go-разработчик — Repository. Его суть проста: создаём прослойку между бизнес-логикой и хранилищем данных (БД, файлы, внешние API).

Зачем это нужно?

  • Тестируемость: можно легко подменить реальную БД на мок.
  • Гибкость: переезд с PostgreSQL на MongoDB? Меняем только репозиторий.
  • Чистота кода: handler не знает, откуда берутся данные — это задача репозитория.

Пример реализации

Допустим, у нас есть модель пользователя:

type User struct {
    ID    int64
    Name  string
    Email string
}

Определяем интерфейс репозитория:

type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*User, error)
    Create(ctx context.Context, user *User) error
    Update(ctx context.Context, user *User) error
    Delete(ctx context.Context, id int64) error
}

И его реализацию для PostgreSQL:

type PostgresUserRepo struct {
    db *sql.DB
}

func (r *PostgresUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
    user := &User{}
    err := r.db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id).
        Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        return nil, fmt.Errorf("get user by id: %w", err)
    }
    return user, nil
}

Теперь ваш handler или сервис работает только с интерфейсом UserRepository, а не с конкретной БД. Это и есть архитектурный паттерн Repository в Go в действии.

2. Service Layer: бизнес-логика под контролем

Если Repository отвечает за данные, то Service Layer — за бизнес-правила. Этот паттерн собирает разрозненные операции в осмысленные действия.

Проблема без Service Layer

Часто вижу код, где handler делает всё: валидирует запрос, проверяет права, пишет в БД и логирует. Это нарушает принцип единственной ответственности (SRP).

Как это исправить?

Вводим сервисный слой:

type UserService interface {
    Register(ctx context.Context, name, email string) (*User, error)
    GetProfile(ctx context.Context, userID int64) (*User, error)
}

type userService struct {
    repo UserRepository
    mail MailSender
}

func (s *userService) Register(ctx context.Context, name, email string) (*User, error) {
    // 1. Валидация
    if name == "" || email == "" {
        return nil, ErrInvalidInput
    }

    // 2. Бизнес-логика: проверяем, не занят ли email
    existing, _ := s.repo.GetByEmail(ctx, email)
    if existing != nil {
        return nil, ErrEmailAlreadyExists
    }

    // 3. Создаём пользователя
    user := &User{Name: name, Email: email}
    if err := s.repo.Create(ctx, user); err != nil {
        return nil, fmt.Errorf("create user: %w", err)
    }

    // 4. Побочный эффект: отправляем приветственное письмо
    go s.mail.SendWelcome(user.Email, user.Name)

    return user, nil
}

Теперь handler только вызывает userService.Register(), а вся архитектура Go-приложения становится прозрачной и тестируемой.

3. Worker Pool: укрощаем горутины

Go славится лёгкостью конкурентности, но запуск тысячи горутин без контроля — прямой путь к панике. Worker Pool — паттерн, который ограничивает количество одновременно работающих горутин.

Когда применять?

  • Обработка очередей задач (например, отправка email).
  • Парсинг большого количества страниц.
  • Работа с rate-limited API.

Реализация Worker Pool на Go

type Pool struct {
    jobs    chan func()
    quit    chan struct{}
    wg      sync.WaitGroup
}

func NewPool(workers int) *Pool {
    p := &Pool{
        jobs: make(chan func(), 100),
        quit: make(chan struct{}),
    }

    for i := 0; i < workers; i++ {
        p.wg.Add(1)
        go p.worker(i)
    }

    return p
}

func (p *Pool) worker(id int) {
    defer p.wg.Done()
    for {
        select {
        case job := <-p.jobs:
            job()
        case <-p.quit:
            return
        }
    }
}

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

func (p *Pool) Shutdown() {
    close(p.quit)
    p.wg.Wait()
}

Использование:

pool := NewPool(5)
for i := 0; i < 20; i++ {
    taskID := i
    pool.Submit(func() {
        fmt.Printf("Task %d выполняется\n", taskID)
        time.Sleep(time.Second)
    })
}
pool.Shutdown()

Этот паттерн Worker Pool в Golang спасает от перегрузки системы и делает конкурентность предсказуемой.

4. Pipeline: потоковая обработка данных

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

Классический пример: конвейер обработки данных

func generate(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case out <- n:
            case <-ctx.Done():
                return
            }
        }
    }()
    return out
}

func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)                

Блоги

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