Перейти к содержанию

Паттерны проектирования в 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'а БД-транзакции.

Проблема:

tx.Commit()                           // commit прошёл
kafka.Publish("user.created", user)   // упало → событие потеряно

Решение: записать событие в таблицу 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).