Метод представляет собой функцию, привязанную к конкретному типу. Методы позволяют связывать поведение и данные типа в самом типе, обеспечивая инкапсуляцию. Если вы знакомы с программированием на Python или С++/C#/Java, то методы в Go будут похожи на методы классов, за некоторыми исключениями.
Note
В Python, например, объект класса передаётся в метод через явно указанную первую переменную
self
. В Go для этого реализован отдельный синтаксис — метод должен быть объявлен в том же пакете, что и тип, к которому он привязывается. В Python же метод определяется в теле класса. С++ требует явного указания, к какому классу принадлежит метод, однако указатель на объект класса, для которого вызван метод, передаётся неявно через переменнуюthis
.
Синтаксис метода типа похож на синтаксис обычной функции, но добавляется получатель (receiver) после ключевого слова func
. Можно сказать, что получатель — это ещё один аргумент функции.
Методы могут быть не только у типа данных «структура», что удивит людей, ранее писавших на других языках ООП. Так как методы определяются в том же пакете, что и тип, то методам доступны все неэкспортируемые элементы в этом пакете.
Важно отметить, что, по сути, методы не предоставляют механизма сокрытия. Если мы определим два типа в одном пакете, то неэкспортируемые поля и методы одного типа будут доступны методам другого типа. Сокрытие реализуется через неэкспортируемые элементы пакета.
Приведём пример каноничной для#Go реализации перечисления (enum) с минимальным набором методов:
Тип DeliveryState
эквивалентен типу string
, поэтому можно получить его экземпляр простым преобразованием типов. Приведём пример такого преобразования и применения функции валидации:
Методы структур
Приведём ещё один пример использования методов. Методы определяются для пользовательских типов данных, и чаще всего в этой роли выступают структуры. В целом методы структур явно не отличаются от методов других типов, но есть нюансы, которые стоит разобрать.
Определим тип кольцевой буфер
с минимальным набором методов и приведём пример кода:
[0]: []
[1]: [1]
[2]: [1 2]
[3]: [1 2 3]
[4]: [1 2 3 4]
[4]: [2 3 4 5]
В примере выше показано, что у метода может быть два варианта получателя:
- Получатель по значению (
b CircularBuffer
). - Получатель по указателю (
b* CircularBuffer
).
Получатель по значению
Note
Вызов в обоих случаях одинаков. Однако при
b.Method()
для получателей по указателю компилятор фактически создаёт такой вызов(&b).Method()
, то есть в метод передаётся указатель на объект, для которого вызывается метод.
У типа CircularBuffer
есть два метода с получателем по значению (value receiver): GetCurrentSize
, GetValues
. Для этих методов получатель принимает вид func (b CircularBuffer)
.
Оба метода не изменяют состояние объекта с точки зрения логики типа. С точки зрения языка методы с таким получателем не могут изменить состояние объекта, который вызвал данный метод.
Переменная b
(получатель метода) содержит копию экземпляра CircularBuffer
, поэтому любое изменение полей b
приведёт к изменению локальной (для метода) копии объекта. Если поле структуры имеет ссылочный тип, то изменение этого поля в локальной копии отразится на оригинальной переменной. Для примера добавим метод, который явно устанавливает значение элемента буфера (слайса values
) по индексу:
[-1 -2 0 0 0]
Почему так происходит? Если поле — это указатель или имеет ссылочный тип (map
, chan
, slice
), то оно будет ссылаться на те же самые объекты и в копии переменной.
Получатель по указателю
У типа CircularBuffer
есть один метод с получателем по указателю (pointer receiver): AddValue
. Получатель такого типа принимает вид func (b *CircularBuffer)
. Функция метода получает указатель на экземпляр типа и, как следствие, может изменять его поля.
Важно отметить, что методы с получателем по указателю могут быть не только у структур, но и у любых других пользовательских типов:
[1 2]
Метод с получателем по значению получает копию объекта, для которого он был вызван, поэтому такой метод не может поменять значение вызывающего объекта. Однако если среди полей объекта есть ссылочные типы, то их изменение повлияет на исходный объект. Метод с получателем по указателю получает указатель на объект, для которого он был вызван, и работает с указателем.
- Вызов метода с получателем по значению для указателя на объект будет эквивалентен вызову
(*b).Method()
. - Вызов метода с получателем по указателю для значения объекта будет эквивалентен вызову
(&b).Method()
.
ООП и методы
Как было показано выше, методы отвечают за инкапсуляцию в реализации парадигмы ООП в Go и действительно позволяют связать поведение и данные в одном типе.
Note
Обратите внимание: так как функция в Go является объектом первого порядка, получаем некоторые возможности для расширения возможностей программирования и получения более изящного кода.
Функция как поле структуры
Для примера создадим структуру следующего вида:
Вызов функции-поля внешне не отличается от вызова метода, однако есть существенные особенности:
- Функция-поле не имеет доступа к вызвавшему её объекту, если он не передан в неё явно.
- Функция-поле может быть динамически переопределена во время работы программы. Это позволяет использовать, например, функции из других пакетов.
- Функция-поле может быть пустой. Тогда её вызов создаст панику.
Функция как поле структуры может использоваться для изменения поведения объекта на лету.
Передача метода как аргумента функции
Мы уже знаем, что функция в Go может быть передана в качестве аргумента в другую функцию. Методы работают аналогичным образом.
Рассмотрим пример. У нас есть некоторая функция-обработчик, которая принимает число и функцию:
Создадим новый кольцевой буфер из примера выше:
Здесь показан очень важный момент — метод был передан как функция в функцию-обработчик. При этом он сохранил привязку к конкретному экземпляру структуры, методом которой он является.
Тип аргумента обработчика — это тип функции, и получатель этой функции может быть любым. То есть типы методов совпадают, если совпадают их аргументы и возвращаемые значения. Тип получателя при этом не учитывается. Такой подход часто применяется при построении серверов, где методы регистрируются как обработчики входящих запросов.
Обратите внимание, у типа CircularBuffer
есть методы с получателями и по значению, и по указателю. Такое смешивание разных типов методов допускается стандартом языка, но не принято в Go-сообществе. Придерживайтесь соглашений, которым следует ваша команда разработчиков.
Если у объекта все методы только с получателем по значению и все поля неэкспортируемые, можно сказать, что этот объект неизменяем (immutable). И наоборот, объект изменяем (mutable), если все методы с получателем по указателю или одно из полей экспортируемое.
Наличие методов по указателю не обязывает вас создавать его экземпляры через указатель:
Value: 100
Таким образом, методы с получателем по указателю и по значению работают практически одинаково, за исключением того, что передаётся в функцию-метод — значение или указатель.
Note
Методы — это один из основных и, можно сказать, наиболее используемый инструмент в Go для построения сложных программ. При этом методы не являются членами класса, как в Python, и могут быть созданы для любого типа данных. Доступ к переменным из метода осуществляется через получателя.
📂 Go | Последнее изменение: 26.08.2024 19:39