Паттерны проектирования в Go¶
Принцип¶
Не все 23 паттерна GoF одинаково полезны в Go. Часть из них (Singleton, AbstractFactory, Visitor) — артефакты Java/C++ времён. В Go реальный набор сильно меньше.
Strategy¶
Поведение в виде интерфейса, реализация подменяется.
type Payer interface {
Charge(amount int) error
}
type StripePayer struct{}
func (s *StripePayer) Charge(a int) error { /* Stripe API */ }
type CloudPaymentsPayer struct{}
func (c *CloudPaymentsPayer) Charge(a int) error { /* CP API */ }
type Checkout struct {
payer Payer
}
func (c *Checkout) Pay(amount int) error { return c.payer.Charge(amount) }
Когда: разные провайдеры одного действия (платежи, отправка, нотификации).
Repository¶
DAO-слой, прячет БД за интерфейсом.
type UserRepo interface {
Get(ctx context.Context, id int64) (*User, error)
Save(ctx context.Context, u *User) error
}
type pgUserRepo struct{ db *pgxpool.Pool }
func (r *pgUserRepo) Get(...) {...}
Когда: всегда. Тесты пишутся через мок UserRepo, без реальной БД.
Observer / Pub-Sub¶
В Go обычно реализуется через каналы или внешний брокер.
type Subscriber chan Event
type Bus struct {
mu sync.RWMutex
subs []Subscriber
}
func (b *Bus) Publish(e Event) {
b.mu.RLock()
defer b.mu.RUnlock()
for _, s := range b.subs {
select {
case s <- e:
default: // не блокируемся, если получатель медленный
}
}
}
Когда: внутри сервиса. Между сервисами — Kafka/NATS/Redis Streams.
Factory¶
В Go это просто конструктор-функция NewX().
func NewServer(cfg Config, log *slog.Logger) (*Server, error) {
if cfg.Port == 0 { return nil, errors.New("port required") }
return &Server{cfg: cfg, log: log}, nil
}
Outbox pattern¶
Гарантия доставки события в очередь после commit'а БД-транзакции.
Проблема:
Решение: записать событие в таблицу outbox внутри той же транзакции,
отдельный воркер читает таблицу и публикует в Kafka.
CREATE TABLE outbox (
id BIGSERIAL PRIMARY KEY,
aggregate_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
published_at TIMESTAMPTZ
);
// внутри одной транзакции:
INSERT INTO users (...) VALUES (...);
INSERT INTO outbox (aggregate_id, event_type, payload) VALUES (...);
COMMIT;
// фоновый воркер:
SELECT * FROM outbox WHERE published_at IS NULL ORDER BY id LIMIT 100;
// публикует в Kafka, помечает published_at
При at-least-once в downstream нужна идемпотентность.
Decorator (middleware)¶
В Go это middleware на http.Handler:
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
mux := http.NewServeMux()
mux.Handle("/api/users", LoggingMiddleware(usersHandler))
Что НЕ нужно копировать из Java/C++¶
- Singleton — обычно решается через
init()или sync.Once. - Abstract Factory — over-engineering для Go.
- Visitor — практически не встречается, есть
interface{}+ type switch. - Builder — обычно достаточно
Options struct{}+New(opts Options).