Объявление метода
Метод это функция с получателем: дополнительным параметром, который пишется перед именем метода. Получатель привязывает метод к типу.
Имя получателя обычно состоит из одной-двух букв, взятых из имени типа (c для Circle). Go не использует this или self, и руководства по стилю не советуют такие имена.
Поскольку у Circle есть метод String() string, он удовлетворяет fmt.Stringer, и fmt.Println печатает результат этого метода вместо {2}.
Получатель-значение и получатель-указатель
Получатель-значение (c Circle) получает копию. Получатель-указатель (c *Circle) получает адрес, поэтому изменения полей сохраняются.
IncByValue компилируется и не делает ничего заметного: классический баг. Если метод должен менять получатель, ему нужен получатель-указатель.
Вызовы также показывают два удобства Go. c.Inc() на адресуемой переменной переписывается в (&c).Inc(), а p.Value() на указателе переписывается в (*p).Value(). Писать & или * только ради вызова метода приходится редко.
Автоматическому & нужно что-то адресуемое. Элемент мапы или составной литерал таковыми не являются:
m := map[string]Counter{"a": {}}
m["a"].Inc() // compile error: cannot call pointer method Inc on Counter
Counter{}.Inc() // same error: a composite literal is not addressable
Храните в мапе указатели (map[string]*Counter) или скопируйте значение, измените его и запишите обратно.
Какой получатель выбрать
| Получатель-указатель, когда | Получатель-значение, когда |
|---|---|
| Метод меняет получатель | Тип маленький, и его методы его не меняют (time.Time, точка) |
| Структура большая (копирование дорого) | Тип это map, func или chan (уже ссылка) |
Тип содержит sync.Mutex или подобное | Нужно, чтобы значение вело себя как примитив и его можно было безопасно копировать |
| Другие методы типа уже используют указатели |
Go FAQ и стандартная библиотека следуют одному практическому правилу: единообразие. Если хоть одному методу нужен получатель-указатель, дайте указатели всем методам, чтобы набор методов был предсказуемым. go vet отмечает структуру с sync.Mutex, которая копируется через получатель-значение (passes lock by value).
Наборы методов и интерфейсы
Сильнее всего разница проявляется в интерфейсах. Набор методов T содержит методы с получателем-значением. Набор методов *T содержит методы и с получателем-значением, и с получателем-указателем.
Если убрать & перед Square{Side: 3} в этом слайсе, код не скомпилируется:
cannot use Square{…} (value of struct type Square) as Shape value in array or slice literal: Square does not implement Shape (method Area has pointer receiver)
Причина: интерфейс может хранить копию Square, которая не адресуема, поэтому Go не может взять её адрес, чтобы вызвать метод указателя. Продолжение этой истории на странице об интерфейсах.
Методы на типах, которые не являются структурами
Методы могут быть у любого именованного типа, объявленного в вашем пакете, а не только у структур.
Ограничения: тип должен быть определён в том же пакете, что и метод, а его базовый тип не может быть указателем или интерфейсом. Добавить методы к int, string или time.Time из своего пакета нельзя. Путь Go: обернуть в новый именованный тип. String() на целочисленном типе также даёт перечислениям читаемый вывод; см. перечисления через iota.
Значения методов и выражения методов
Метод, привязанный к конкретному получателю, это функциональное значение:
Значение метода с получателем-значением копирует получатель в момент создания, поэтому f по-прежнему говорит Ana. С получателем-указателем сохранился бы указатель, и последующие изменения были бы видны. Значения методов удобны для колбэков: http.HandleFunc("/", srv.handleIndex).
Частые ошибки
- Получатель-значение у метода, который должен менять состояние. Компилируется и молча меняет копию.
- Смешение видов получателей. Становится неясно, что удовлетворяет интерфейсу,
Tили*T. Выберите один вариант для типа. - Вызов метода указателя на nil-указателе. Это разрешено, и метод выполняется с nil-получателем. Паника возникает, только когда он обращается к полю. Некоторые типы (например, nil-безопасный связный список) используют это намеренно; большинство такого не ожидают.
Часто задаваемые вопросы
Чем метод отличается от функции в Go?
У метода есть получатель, который пишется между func и именем: func (c Circle) Area() float64. Метод вызывают на значении (c.Area()), он входит в набор методов типа, и именно через методы тип удовлетворяет интерфейсам. В остальном метод ведёт себя как функция, первый аргумент которой это получатель.
Когда в Go использовать получатель-указатель?
Когда метод должен изменить получатель, когда структура настолько большая, что копировать её при каждом вызове расточительно, или когда тип содержит то, что нельзя копировать, например sync.Mutex. Если хотя бы одному методу типа нужен получатель-указатель, обычно советуют ради единообразия сделать указателями все получатели.
Почему мой тип не реализует интерфейс, если у метода получатель-указатель?
Методы с получателем-указателем входят в набор методов *T, а не T. Поэтому var s Shape = Square{} не компилируется с ошибкой Square does not implement Shape (method Area has pointer receiver). Присвойте указатель: var s Shape = &Square{}.
Можно ли объявлять методы на встроенных типах в Go?
Напрямую нет. Методы можно объявлять только на именованных типах, определённых в том же пакете. Сначала определите свой тип, type Celsius float64, а потом добавляйте к нему методы. Добавить методы к int, string или типу из другого пакета нельзя.