Паттерны проектирования в 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