Язык Go — кратко. Часть 8. Структуры и интерфейсы

В языке Go очень своеобразная модель ООП. Её называют «утиной типизацией».

Здесь нет наследования. Зато есть встраивание, которое частично заменяет наследование. Здесь нет перегрузки и переопределения методов. Всё упрощено до предела, чтобы уменьшить возможность построить сложно поддерживаемую систему, изменения в базовых классах которой вполне могут привести к нарушениям и неожиданному поведению в работе программы.

Что мы имеем для реализации ООП в Go?

Структуры и интерфейсы.

Структуры

В подавляющем большинстве случаев, когда нам нужен свой тип для хранения и обработки данных, мы работаем со структурами. Структура может содержать (агрегировать) некоторый набор полей. У структуры могут быть свои методы. Структура может реализовать интерфейсы. Структура может встраивать в себя другую структуру (или тип).

Первый момент. Если метод должен изменять данные, тип приёмника пишем в виде указателя (метод AddString()).

Второй момент. Тип StringList встраивает в себя тип Strings. При этом он получает от Strings его методы IsEmpty() и String(). Причём, метод String() переопределяется. И, да, StringList имея реализацию метода String() по факту реализует интерфейс Stringer. Мы нигде не указывали, что наш тип будет отвечать какому-то интерфейсу. Это та самая «утиная типизация». Её принцип — «если это выглядит как утка, плавает как утка и крякает как утка, то это, возможно, и есть утка». То есть, если наш тип реализует все требуемые методы интерфейса (естественно, с их сигнатурами), значит он реализует этот интерфейс.

Третий момент. Обратили внимание на то, как разрешаются конфликты имён полей (и методов)? Если поле (метод) не конфликтует, то к нему можно обратиться в сокращённой форме, как будто оно поле непосредственно этого типа (радиус). Если же имеет место конфликт имён, то указываем точное поле (x и y).

Четвёртый момент. При необходимости проведения каких-то операций для каждого объекта на этапе его создания, можно написать конструктор типа. Здесь это NewStringList(). Конструкторы именуются начиная с «New». Если конструктор единственный в пакете и типов в пакете больше не планируется, можно просто так и сделать — New(). Пример — errors.New(). Когда могут пригодиться конструкторы? Если нужно произвести какую-то валидацию данных или когда надо задать ненулевые значения по умолчанию.

Интерфейсы

Интерфейсы мы используем там, где нам требуется работать с разнородными объектами, имеющими какое-то общее поведение. Например, у нас есть несколько типов задач и некий многопоточный пул для их выполнения. Чтобы не рисовать по пулу на каждый тип задач и не плодить лишние сущности, мы можем сделать примерно так:

Интерфейсы принято называть в стиле «делающий то-то», добавляя окончание -er. Stringer — «делающий String». Executer — «делающий Execute».

В Go вообще все типы (включая базовые) отвечают («реализуют») пустому интерфейсу. Именно поэтому можно создать универсальное хранилище. Неэффективное (из-за затрат на преобразование обратно к реальному типу), зато универсальное. 😉

Кстати,  функции в пакетах fmt и log работают через пустые интерфейсы.

Leave a Reply

Ваш e-mail не будет опубликован. Обязательные поля помечены *