Паттерны проектирования в Go: когда и зачем их использовать

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

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

Начать курс

Паттерны проектирования в Go: не «религия», а инструмент

Когда я только начинал писать на Go, меня преследовало ощущение, что все эти «банды четырех» и их 23 паттерна — это что-то из мира Java и C++. Go же такой простой и лаконичный — зачем нам эти сложности? Но чем глубже я погружался в реальные проекты (микросервисы, CLI-утилиты, обработчики API), тем чаще ловил себя на мысли: «Вот тут бы пригодился фабричный метод» или «Эту гору if-else можно заменить стратегией».

Паттерны проектирования в Go — это не догма и не попытка натянуть ООП на функциональный язык. Это проверенные решения типовых проблем. Они помогают сделать код тестируемым, гибким и понятным для коллег. И да, Go накладывает свой отпечаток: из-за отсутствия классического наследования и generics (до версии 1.18) некоторые паттерны трансформируются, а некоторые становятся избыточными.

В этой статье мы не будем перечислять все 23 паттерна. Вместо этого разберем 5 самых востребованных шаблонов в Golang, которые реально пригодятся в боевых проектах. Вы увидите, когда их стоит применять, а когда — бежать без оглядки.

Паттерн Singleton в Go: не просто синглтон, а синглтон по-гошному

Singleton гарантирует, что у структуры будет только один экземпляр. В Go это часто используют для конфигов, пулов соединений или логгеров. Но есть нюанс: в Go нет статических классов, зато есть sync.Once — идеальный инструмент для ленивой инициализации.

Проблема без Singleton

Представьте, что у вас есть структура Config, которая читает настройки из файла. Если каждый пакет будет создавать свой экземпляр, вы либо прочитаете файл 10 раз (медленно), либо получите race condition.

Решение с Singleton

package config

import (
    "sync"
    "encoding/json"
    "os"
)

type Config struct {
    Port int    `json:"port"`
    Env  string `json:"env"`
}

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        file, _ := os.Open("config.json")
        defer file.Close()
        instance = &Config{}
        json.NewDecoder(file).Decode(instance)
    })
    return instance
}

Когда использовать: когда нужна гарантированная единственность экземпляра (конфиги, кэши, фабрики).

Когда не надо: для всего подряд. Синглтон — это глобальное состояние, которое усложняет тестирование. Если можно передать зависимость через конструктор — передавайте.

Паттерн Factory: когда создание объектов становится сложным

Factory Method и Abstract Factory — это про инкапсуляцию логики создания объектов. В Go это особенно актуально, так как у нас нет конструкторов в классическом понимании. Вместо этого мы используем функции-конструкторы с префиксом New.

Пример: создание уведомлений

Допустим, ваша система отправляет уведомления через Email, SMS и Push. Вместо того чтобы размазывать логику выбора по коду, мы используем фабрику.

package notification

import "fmt"

type Notifier interface {
    Send(message string) error
}

type EmailNotifier struct{}

func (e EmailNotifier) Send(message string) error {
    fmt.Println("Sending email:", message)
    return nil
}

type SMSNotifier struct{}

func (s SMSNotifier) Send(message string) error {
    fmt.Println("Sending SMS:", message)
    return nil
}

func NewNotifier(typ string) Notifier {
    switch typ {
    case "email":
        return EmailNotifier{}
    case "sms":
        return SMSNotifier{}
    default:
        return nil
    }
}

Когда использовать: когда логика создания сложная (зависит от типа, версии, конфига).

Когда не надо: если у вас всего один тип объекта. Просто пишите MyStruct{} или NewMyStruct() без фабрики.

Паттерн Strategy: убиваем if-else и switch

Strategy — это паттерн, который позволяет менять поведение объекта на лету, вынося алгоритмы в отдельные структуры. В Go это делается через интерфейсы.

Пример: обработка платежей

У вас есть три платежных системы: Stripe, PayPal, Qiwi. Вместо гигантского switch:

package payment

import "fmt"

type PaymentStrategy interface {
    Pay(amount float64) string
}

type Stripe struct{}
func (s Stripe) Pay(amount float64) string {
    return fmt.Sprintf("Paid %.2f via Stripe", amount)
}

type PayPal struct{}
func (p PayPal) Pay(amount float64) string {
    return fmt.Sprintf("Paid %.2f via PayPal", amount)
}

type PaymentContext struct {
    strategy PaymentStrategy
}

func (c *PaymentContext) SetStrategy(s PaymentStrategy) {
    c.strategy = s
}

func (c *PaymentContext) ExecutePayment(amount float64) string {
    return c.strategy.Pay(amount)
}

Когда использовать: когда есть несколько алгоритмов, которые могут меняться во время выполнения, или когда код начинает напоминать «спагетти» из условий.

Когда не надо: если алгоритмов 2-3 и они никогда не меняются. Простой if-else читается быстрее.

Паттерн Builder: собираем сложные объекты

Builder — спасение для структур с 10+ полями, особенно если некоторые из них опциональны. В Go это часто реализуют через функциональные опции (functional options pattern), но классический Builder тоже жив.

Пример: конфигурация HTTP-клиента

package httpclient

import "time"

type Client struct {
    timeout    time.Duration
    retries    int
    baseURL    string
    headers    map[string]string
}

type ClientBuilder struct {
    client Client
}

func NewBuilder() *ClientBuilder {
    return &ClientBuilder{client: Client{
        timeout: 30 * time.Second,
        retries: 3,
    }}
}

func (b *ClientBuilder) WithTimeout(t time.Duration) *ClientBuilder {
    b.client.timeout = t
    return b
}

func (b *ClientBuilder) WithRetries(r int) *ClientBuilder {
    b.client.retries = r
    return b
}

func (b *ClientBuilder) WithBaseURL(url string) *ClientBuilder {
    b.client.baseURL = url
    return b
}

func (b *ClientBuilder) Build() Client {
    return b.client
}

Когда использовать: когда у структуры много полей, особенно если большинство из них опциональны.

Когда не надо: если полей меньше 5. Функция NewClient(timeout, retries, url) будет проще и понятнее.

Паттерн Decorator: расширяем функциональность без наследования

В Go нет классического наследования, но есть композиция и интерфейсы. Decorator позволяет «оборачивать» объект в новые слои поведения, не меняя его исходный код. Это идеально для middleware, логирования, кэширования.

Пример: логирующий HTTP-клиент

package main

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

type Client interface {
    Do(req *http.Request) (*http.Response, error)
}

type LoggingDecor                

Блоги

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