🦫 Самоучитель по Go для начинающих. Часть 9. Структуры и методы. Интерфейсы. Указатели. Основы ООП
В этом уроке самоучителя подробно рассмотрим структуры, методы и интерфейсы в Go, уделим особое внимание их особенностям и применению. В заключение познакомимся с конструкциями type assertion и type switch.
Указатели
Ранее мы уже познакомились с указателями при рассмотрении функций, теперь пришло время изучить их подробнее.
Указатель – это переменная, в которой хранится адрес другой переменной.
В Go для создания и разыменования указателей используется оператор *
, а для получения адреса переменной – оператор &
. Рассмотрим их применение в коде:
Альтернативный способ создать указатель – использовать функцию new
, которая выделяет память под указанный тип и возвращает на неё указатель:
Разыменование – это процесс получения данных из блока памяти, на который указывает адрес указателя. В Go для этих целей необходимо поставить оператор *
перед указателем:
Значения переменных можно изменять через указатель:
При работе с указателями стоит помнить, что в Go всё передается по значению, то есть копируется. Это имеет значение при передаче параметров в функцию, что было подробно рассмотрено в соответствующем пункте шестого урока самоучителя.
В отличие от языка C, в Go нет арифметики указателей, и управление памятью происходит автоматически: она выделяется при создании объектов и освобождается при сборке мусора. По этой причине при работе с объектами малых размеров стоит избегать использования указателей, чтобы снизить нагрузку на сборщик мусора.
Структуры
Структура представляет собой набор полей с определенными названиями и типами данных. В Go она создается с помощью ключевого слова struct
с указанием полей в фигурных скобках:
Рассмотрим различные способы объявления структуры:
Доступ к полям структуры осуществляется с помощью точки:
Получить поля структуры можно с помощью указателя:
Структурные теги
Поля структур могут иметь специальные теги, которые указывают дополнительную информацию для их обработки другим кодом. Они записываются после типа данных следующим образом:
К примеру, при работе с JSON для парсинга полей структуры используется тег json
с указанием определенного ключа:
Сравнение структур
Сравнение структур в Go имеет свои особенности, которые мы рассмотрим на конкретных примерах.
Создадим две структуры с идентичными полями, но с разными названиями:
Два значения одной структуры считаются равными, если равны их соответствующие поля:
При попытке сравнить значения разных структур возникнет ошибка, так как типы значений будут отличаться:
Для корректного сравнения нужно преобразовать одну структуру в другую:
Допустимо сравнение именованных (named) и неименованных (unnamed) структур:
Структуры и пакеты
Ранее мы уже обсуждали организацию кода в Go, сейчас пришло время повторить эту тему в контексте изучения структур.
В Go весь написанный код находится в файлах с расширением .go
, расположенных в пакетах, которые делят программу на связанные модули. Импорт пакета дает возможность обращаться к его внутренним объектам.
Структуры и их поля могут быть экспортируемыми или неэкспортируемыми. Первые доступны за рамками текущего пакета и указываются с заглавной буквы, а вторые – недоступны извне и пишутся с маленькой буквы.
Чтобы лучше понять механизм экспорта и импорта структур, создадим тестовый проект со следующей организацией папок и файлов:
В файле myapp/service/roles.go
создадим две структуры: User будет доступна вне пакета service за исключением поля age
, а другую структуру admin сделаем неэкспортируемой:
В главном файле main.go
попробуем обратиться к различным объектам обеих структур и посмотрим, что получится:
Методы
Вместо классов, в Go используются методы. Они представляют собой функции, связанные с определенными типами (type). Сигнатура метода содержит дополнительный аргумент, который указывает получателя и пишется между ключевым словом func
и названием:
Для лучшего понимания рассмотрим пример, в котором создадим именованный тип слайса целых чисел и реализуем для него метод squareElements
:
На выходе получим ожидаемый результат:
Методы структур
Методы полезны при работе со структурами, так как упрощают работу с их полями, а также улучшают читаемость и связность кода.
В качестве примера создадим структуру User
, реализуем для нее два метода GetUserInfo
и ChangeUserInfo
и вызовем их в main
:
Как вы могли заметить, в коде выше структура передается по значению, поэтому изменение её полей в методе ChangeUserInfo
никак не повлияет на исходную структуру.
Для изменения значений структуры необходимо передать её по указателю:
Интерфейсы
Одна из немаловажных характеристик кода – это его связность. Она показывает, насколько каждый отдельный блок зависит от другого. Если связность кода сильная, то изменение одного его компонента повлияет на множество других, что с высокой вероятностью приведет к ошибкам и запутанности.
Для управления связностью необходимо делить код на независимые блоки, взаимодействующие с помощью определенного интерфейса. Его можно воспринимать как некое соглашение, которое устанавливает правила поведения объектов.
В Go интерфейс представлен типом interface и определяет методы, которые должен иметь другой тип. Иными словами, интерфейс абстрагируется от реализации конкретного объекта и позволяет взаимодействовать с ним с помощью заданных методов.
Применение интерфейсов
Давайте на практике рассмотрим применение интерфейсов и правила работы с ними. Начнем с создания интерфейса Shape
с методом Area
для вычисления площади геометрических фигур:
Первое правило звучит так: если некая сущность реализует все методы интерфейса, то говорят, что она удовлетворяет ему (реализует его). Данный принцип иногда называют «утиная типизация» (duck typing): если это выглядит как утка, плавает как утка и крякает как утка, тогда, вероятно, это и есть утка.
Создадим структуру Rectangle
, удовлетворяющую интерфейсу Shape
:
Второе правило: если объявление параметра функции, переменой или поля структуры имеет интерфейсный тип, то допускается использование объекта любого типа, пока он реализует этот интерфейс.
Это может показаться сложным на словах, поэтому давайте перейдем к примеру, в котором напишем функцию GetArea
для вычисления площади фигур:
Так как параметром функции GetArea
является интерфейсный тип Shape
, это позволяет передавать в качестве аргумента объект, удовлетворяющий интерфейсу Shape
.
К примеру, можно передать в функцию GetArea
экземпляр структуры Rectangle
, которая реализует Shape
, и получить значение площади прямоугольника:
Пустой интерфейс
В Go допускается создание пустого интерфейса interface{}
, который не имеет методов, поэтому любой объект удовлетворяет ему. Пустой интерфейс используется при работе с заранее неизвестным типом данных, поэтому применяется для реализации библиотечных функций.
Обратимся к примеру использования пустого интерфейса в коде:
Приведение и проверка типа
Работа с пустым интерфейсом имеет ряд особенностей. Чтобы в этом убедиться, давайте попробуем создать мапу со значениями типа interface{}
и изменить её элементы по ключу:
В результате получим ошибку со следующим описанием: invalid operation: mp[2] += 100 (mismatched types interface{} and int)
. Она возникла по той причине, что значение мапы по ключу 2 имеет тип interface{}
, а не int.
Чтобы это исправить, нужно воспользоваться конструкцией type assertion
, которая предоставляет доступ к конкретному значению интерфейса. Она предназначена для проверки типа значения и его приведения к требуемому типу. В общем случае её синтаксис выглядит следующим образом:
Если obj
имеет тип T
, то в value
запишется значение этого типа, а ok
станет true
, иначе – ok
будет false
, а value
сохранит нулевое значение типа T
.
Используем type assertion
для приведения типа значения по ключу 2 к целочисленному:
При попытке преобразования к другому типу в переменной ok
будет false
, а value
сохранит нулевое значение указанного типа:
Иногда возникает необходимость проверить соответствие сразу нескольким типам. В таких случаях используется конструкция type switch
. Она имеет синтаксис switch-case
, но в case
проверяется не значение, а тип:
Чтобы лучше понять поведение type switch
, напишем функцию, которая будет для переменной строкового типа выводить её длину, для слайса []int
– его емкость, в остальных случаях – название типа:
Подведём итоги
Структуры, методы и интерфейсы позволяют создавать гибкий и модульный код, облегчают его понимание и дальнейшее масштабирование. В этой статье мы рассмотрели особенности этих концепций и изучили полезные синтаксические конструкции type assertion
и type switch
.
В следующем уроке применим полученные знания для погружения в мир объектно-ориентированного программирования.
Содержание самоучителя
- Особенности и сфера применения Go, установка, настройка
- Ресурсы для изучения Go с нуля
- Организация кода. Пакеты, импорты, модули. Ввод-вывод текста.
- Переменные. Типы данных и их преобразования. Основные операторы
- Условные конструкции if-else и switch-case. Цикл for. Вложенные и бесконечные циклы
- Функции и аргументы. Области видимости. Рекурсия. Defer
- Массивы и слайсы. Append и сopy. Пакет slices
- Строки, руны, байты. Пакет strings. Хеш-таблица (map)
- Структуры и методы. Интерфейсы. Указатели. Основы ООП
- Наследование, абстракция, полиморфизм, инкапсуляция
- Обработка ошибок. Паника. Восстановление. Логирование
- Обобщенное программирование. Дженерики
- Работа с датой и временем. Пакет time
- Интерфейсы ввода-вывода. Буферизация. Работа с файлами. Пакеты io, bufio, os
- Конкурентность. Горутины. Каналы
- Тестирование кода и его виды. Table-driven подход. Параллельные тесты
- Основы сетевого программирования. Стек TCP/IP. Сокеты. Пакет net