SOLID принципы в Go: продвинутое руководство с примерами

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

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

Начать курс

SOLID принципы в Go: продвинутый уровень

SOLID — это пять фундаментальных принципов объектно-ориентированного проектирования, которые помогают создавать гибкий, поддерживаемый и масштабируемый код. В контексте Go, языка с уникальной философией композиции вместо наследования, применение SOLID требует особого подхода. В этой статье мы разберем каждый принцип на продвинутом уровне, с реальными примерами и антипаттернами, характерными для Go-сообщества.

1. Single Responsibility Principle (SRP) в Go

Принцип единственной ответственности гласит: у класса (структуры) должна быть только одна причина для изменения. В Go это часто нарушается в утилитарных пакетах и "божественных объектах".

Антипаттерн: структура с множеством обязанностей

type UserService struct {
    db *sql.DB
    cache *redis.Client
    logger *log.Logger
}

func (s *UserService) CreateUser(ctx context.Context, u User) error {
    // 1. Валидация
    // 2. Хеширование пароля
    // 3. Сохранение в БД
    // 4. Отправка email
    // 5. Логирование
    return nil
}

Решение: разделите ответственности на отдельные сервисы и используйте композицию.

type UserValidator interface {
    Validate(u User) error
}

type PasswordHasher interface {
    Hash(password string) (string, error)
}

type UserRepository interface {
    Save(ctx context.Context, u User) error
}

type EmailSender interface {
    SendWelcomeEmail(ctx context.Context, email string) error
}

type UserService struct {
    validator   UserValidator
    hasher      PasswordHasher
    repo        UserRepository
    emailSender EmailSender
    logger      Logger
}

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

2. Open-Closed Principle (OCP) через интерфейсы

Принцип открытости/закрытости: сущности должны быть открыты для расширения, но закрыты для модификации. В Go это достигается через интерфейсы и внедрение зависимостей.

Плохой пример: жесткое связывание

type PaymentProcessor struct {
    paypalAPI *PayPalAPI
}

func (p *PaymentProcessor) Process(amount float64) error {
    return p.paypalAPI.Charge(amount)
}

Если нужно добавить Stripe — придется менять код.

Правильный подход: интерфейс + стратегия

type PaymentGateway interface {
    Charge(amount float64) error
    Refund(transactionID string) error
}

type PayPalGateway struct{ /* ... */ }
type StripeGateway struct{ /* ... */ }

func (p *PayPalGateway) Charge(amount float64) error { /* ... */ }
func (s *StripeGateway) Charge(amount float64) error { /* ... */ }

type PaymentService struct {
    gateway PaymentGateway
}

func NewPaymentService(gw PaymentGateway) *PaymentService {
    return &PaymentService{gateway: gw}
}

Теперь можно добавлять новые платежные шлюзы без изменения PaymentService.

3. Liskov Substitution Principle (LSP) в контексте Go

Принцип подстановки Барбары Лисков: объекты в программе должны быть заменяемы экземплярами их подтипов без изменения корректности программы. В Go, где нет классического наследования, LSP проявляется через контракты интерфейсов.

Типичное нарушение: интерфейс с избыточными методами

type Bird interface {
    Fly() error
    Swim() error
}

type Penguin struct{}

func (p *Penguin) Fly() error {
    return errors.New("penguins can't fly")
}

func (p *Penguin) Swim() error {
    return nil
}

Здесь Penguin не может корректно реализовать Fly(), что нарушает LSP.

Решение: разделение интерфейсов

type Flyer interface {
    Fly() error
}

type Swimmer interface {
    Swim() error
}

type Duck struct{}
func (d *Duck) Fly() error { return nil }
func (d *Duck) Swim() error { return nil }

type Penguin struct{}
func (p *Penguin) Swim() error { return nil }

Теперь Penguin реализует только Swimmer, и код, ожидающий Flyer, не сломается.

4. Interface Segregation Principle (ISP) на практике

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

Толстый интерфейс (anti-pattern)

type Worker interface {
    Work() error
    Eat() error
    Sleep() error
}

type Robot struct{}
func (r *Robot) Work() error { return nil }
func (r *Robot) Eat() error { return errors.New("robots don't eat") }
func (r *Robot) Sleep() error { return errors.New("robots don't sleep") }

Правильный подход: тонкие интерфейсы

type Worker interface {
    Work() error
}

type Eater interface {
    Eat() error
}

type Sleeper interface {
    Sleep() error
}

type Human struct{}
func (h *Human) Work() error { return nil }
func (h *Human) Eat() error { return nil }
func (h *Human) Sleep() error { return nil }

type Robot struct{}
func (r *Robot) Work() error { return nil }

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

func ProcessWorkers(workers []Worker) {
    for _, w := range workers {
        w.Work()
    }
}

5. Dependency Inversion Principle (DIP) и внедрение зависимостей

Принцип инверсии зависимостей: модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций.

Нарушение: прямая зависимость от конкретной реализации

type MySQLRepository struct{}

func (r *MySQLRepository) GetUser(id int) User {
    // прямой запрос к MySQL
}

type UserController struct {
    repo *MySQLRepository
}

func (c *UserController) GetUser(id int) User {
    return c.repo.GetUser(id)
}

Правильная реализация с DIP

type UserRepository interface {
    GetUser(id int) (User, error)
    SaveUser(u User) error
}

type MySQLRepository struct{}

func (r *MySQLRepository) GetUser(id int) (User, error) {
    // реализация
}

type UserService struct {
    repo UserRepository
}

func NewUserService(repo UserRepository) *UserService {
    return &                

Блоги

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