Расскажите о принципах SOLID и приведите примеры их применения в Go.

Ответ

S - Single Responsibility Principle (Принцип единственной ответственности)

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

Это делает код более понятным, тестируемым и простым в поддержке.

Пример:

// User представляет данные пользователя
type User struct { /* ... */ }

// UserRepository отвечает только за хранение и извлечение пользователей из БД.
type UserRepository struct { db *sql.DB }
func (r *UserRepository) Save(u User) error { /* ... */ }

// UserNotifier отвечает только за отправку уведомлений пользователю.
type UserNotifier struct { smtpClient *SmtpClient }
func (n *UserNotifier) Notify(u User, message string) error { /* ... */ }

O - Open-Closed Principle (Принцип открытости/закрытости)

Программные сущности (классы, модули, функции) должны быть открыты для расширения, но закрыты для модификации.

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

Пример:

// PaymentProvider - это интерфейс для платежных систем
type PaymentProvider interface {
    ProcessPayment(amount float64) error
}

// Функция, которая использует интерфейс и не знает о конкретных реализациях
func ProcessOrder(p PaymentProvider, orderAmount float64) {
    p.ProcessPayment(orderAmount)
}

// Мы можем легко добавить новый способ оплаты, не меняя функцию ProcessOrder
type StripeProvider struct{}
func (s *StripeProvider) ProcessPayment(amount float64) error { /* ... */ }

type PayPalProvider struct{}
func (p *PayPalProvider) ProcessPayment(amount float64) error { /* ... */ }

L - Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

Объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы.

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

Пример: Если функция ожидает io.ReadCloser, то любая переданная реализация должна корректно считывать данные методом Read() и освобождать ресурсы методом Close(). Реализация, где Close() ничего не делает или паникует, нарушает LSP.

I - Interface Segregation Principle (Принцип разделения интерфейса)

Много специализированных интерфейсов лучше, чем один универсальный.

Не заставляйте клиентов реализовывать методы, которые они не используют. Это один из ключевых принципов идиоматичного Go. Яркий пример — стандартная библиотека (io.Reader, io.Writer, io.Closer).

Пример:

// Хорошо: маленькие, сфокусированные интерфейсы
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Closer interface { Close() error }

// Плохо: один большой "божественный" интерфейс
type File interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Close() error
    Seek(offset int64, whence int) (int64, error)
    Chmod(mode os.FileMode) error
}

D - Dependency Inversion Principle (Принцип инверсии зависимостей)

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

Это достигается через внедрение зависимостей (Dependency Injection), где высокоуровневые компоненты зависят от интерфейсов, а конкретные реализации этих интерфейсов "внедряются" извне.

Пример:

// Абстракция (интерфейс)
type UserRepository interface {
    GetUser(id int) (User, error)
}

// Модуль верхнего уровня (сервис)
type UserService struct {
    repo UserRepository // Зависит от абстракции, а не от конкретной БД
}

// Модуль нижнего уровня (реализация для PostgreSQL)
type PostgresUserRepository struct { /* ... */ }
func (p *PostgresUserRepository) GetUser(id int) (User, error) { /* ... */ }

// В main.go мы "внедряем" зависимость
func main() {
    dbRepo := &PostgresUserRepository{}
    userService := &UserService{repo: dbRepo}
    // ...
}