Что ты думаешь о реализации принципов ООП в языке Go?

Ответ

Go предлагает прагматичный и минималистичный взгляд на объектно-ориентированное программирование, отбросив классические механизмы в пользу простоты и композиции.

Как Go реализует ООП-принципы:

  • Инкапсуляция: Достигается через экспортируемые (с большой буквы) и неэкспортируемые (с маленькой буквы) идентификаторы в пакетах.
  • Наследование: Отсутствует. Вместо него используется композиция через встраивание (embedding) структур.
  • Полиморфизм: Реализуется через неявные интерфейсы. Тип удовлетворяет интерфейсу, просто реализуя его методы, что обеспечивает слабую связанность.

Пример композиции и интерфейсов в Go:

type Writer interface {
    Write([]byte) (int, error)
}

// FileWriter не объявляет явно, что реализует Writer
type FileWriter struct {
    file *os.File
}

func (fw FileWriter) Write(data []byte) (int, error) {
    return fw.file.Write(data) // Неявная реализация интерфейса Writer
}

// Logger использует композицию (встраивание) и зависит от абстракции (интерфейса)
type Logger struct {
    Writer // Встраивание Writer
    Prefix string
}

func (l Logger) Log(msg string) {
    message := fmt.Sprintf("[%s] %sn", l.Prefix, msg)
    l.Write([]byte(message)) // Вызов метода встроенного интерфейса
}

Моё мнение: Такой подход идеален для создания читаемых, эффективных и легко поддерживаемых сервисов, где важна простота и явность. Он отлично подходит для микросервисных архитектур и системного программирования. Однако для сложных предметных областей с глубокими иерархиями сущностей (например, в enterprise-приложениях) отсутствие классического наследования и generics (до версии 1.18) иногда приводит к дублированию кода или менее выразительным абстракциям. Go — это выбор в пользу ясности и продуктивности команды, иногда в ущерб академической полноте ООП.

Ответ 18+ 🔞

А, слушай, про Go и его ООП! Ну это вообще отдельная тема, ёпта. Я тебе так скажу — ребята из Google взяли, посмотрели на эту всю классическую объектно-ориентированную магию с её наследованиями, виртуальными функциями и прочей ебушкой-воробушкой, и сказали: «Да похуй, ребята, мы сделаем по-своему». И сделали. И знаешь что? В чём-то они правы, блядь.

Вот смотри, как они инкапсуляцию делают. Всё гениально и просто, как палка. Если идентификатор с большой буквы — он экспортируется, виден всем. С маленькой — сиди, сука, в своём пакете, и никому не показывайся. Никаких private, protected, public на три страницы. Два состояния: «видно» и «не видно». И доверия ебать ноль — ты сам решаешь, что выставлять наружу. Никаких хитрожопых геттеров и сеттеров по умолчанию, хочешь — напиши сам функцию. Прямота, ядрёна вошь.

А теперь самое вкусное — наследования-то нету! Вообще! Они его выкинули нахуй с горы. Вместо этого у них композиция через встраивание, или embedding. Это как взять одну структуру и просто вставить её в другую, как детальку. Не «является», а «имеет в составе». И от этого голова болит меньше, честно. Классическое наследование — это часто пиздопроебибна, когда ты в дебрях иерархии на десятом уровне уже забываешь, откуда у тебя этот метод взялся. А тут всё на виду.

Ну и полиморфизм у них — просто песня, блядь. Интерфейсы неявные. Это гениально. Ты не говоришь, что твой тип FileWriter реализует интерфейс Writer. Ты просто берёшь и пишешь для него метод Write. Всё. Если метод подходит — поздравляю, твой тип теперь автоматически удовлетворяет интерфейсу. Это как если ты умеешь жарить яичницу — ты повар, и неважно, есть у тебя диплом или нет. Слабая связанность, всё красиво.

Вот, смотри на этот код, он тут как раз про это:

type Writer interface {
    Write([]byte) (int, error)
}

// FileWriter не объявляет явно, что реализует Writer
type FileWriter struct {
    file *os.File
}

func (fw FileWriter) Write(data []byte) (int, error) {
    return fw.file.Write(data) // Неявная реализация интерфейса Writer
}

// Logger использует композицию (встраивание) и зависит от абстракции (интерфейса)
type Logger struct {
    Writer // Встраивание Writer
    Prefix string
}

func (l Logger) Log(msg string) {
    message := fmt.Sprintf("[%s] %sn", l.Prefix, msg)
    l.Write([]byte(message)) // Вызов метода встроенного интерфейса
}

Видишь? Logger просто встроил в себя Writer. У него теперь есть все методы Writer'а. И работает он с любой херовой, прости господи, штукой, у которой есть метод Write. Это и есть композиция и полиморфизм в стиле Go.

А теперь моё мнение, ёбать колотить: Такой подход — он идеален, когда тебе нужно, чтобы пять человек посмотрели на код и сразу поняли, что происходит. Никакой магии, всё на поверхности. Для микросервисов, для всяких системных утилит, для сетевого говна — это овердохуища. Код читаемый, эффективный, и поддерживать его не пиздец как сложно.

Но! Есть и обратная сторона, чувак. Если тебе нужно смоделировать какую-нибудь ебаную сложную предметную область, с кучей сущностей, которые всё-таки в какие-то иерархии складываются, то иногда начинаешь чувствовать себя немного... ограниченно. Приходится копипастить код или выкручиваться. И да, generics-то только недавно добавили, до этого вообще был терпения ноль ебать. Так что Go — это такой осознанный выбор. Выбор в пользу того, чтобы вся команда, от джуна до сеньора, быстро врубалась и не стреляла себе в ногу. А академическую красоту и полноту ООП они, можно сказать, слегка послали нахуй. И, знаешь, во многих случаях это работает на удивление пиздец как хорошо.