Как стать автором
Обновить

Pointer и Value семантика в определении получателя метода

Время на прочтение4 мин
Количество просмотров5.4K
Создание новых типов данных — важная часть работы каждого программиста. В большинстве языков определение типа состоит из описания его полей и методов. В Golang помимо этого нужно решить, какую семантику получателя для методов нового типа использовать: значение (value) или указатель (pointer). На первый взгляд это решение может показаться второстепенным, ведь в большинстве случаев программа будет работать при любой семантике получателя. Поэтому многие пропускают этот момент и пишут код, так и не разобравшись до конца, на что влияет семантика получателя метода. А чтобы разобраться, нужно немного углубиться в то, как устроен Golang.

Рассмотрим небольшой пример. Определим структуру cat с одним полем Name и методом sayHello(person string). Здесь и далее методом я буду называть функцию, связанную с определенным типом, объектом — переменную, которая имеет методы, а получателем метода будет называться переменная, указанная в круглых скобках после слова func в описании метода.

type cat struct {
	Name string
}

func (c *cat) sayHello(person string) {
	fmt.Println(fmt.Sprintf("Meow, meow, %s!", person)
}

Если определить указатель на cat и запросить у него поле Name, то, очевидно, мы получим ошибку, так как поле вызывается у nil:


var c *cat // c=nil
fmt.Println(c.Name)
//panic: runtime error: invalid memory address or nil pointer dereference

https://play.golang.org/p/L3FnRJXKqs0

Однако при вызове у этой же переменной метода sayHello() ошибки не будет:


var c *cat // c=nil
c.sayHello(“Human”)
//Meow, meow, Human!

https://play.golang.org/p/EMoFgKL1HEi

Почему в данном примере у nil можно вызвать метод и как это объясняется с точки зрения архитектуры самого языка? Это становится возможным потому, что метод в Go — это синтаксический сахар, или, другими словами, обертка вокруг функции, имеющей одним из аргументов получатель. При вызове метода c.sayHello(“Human”) на самом деле будет вызываться конструкция (*cat).sayHello(c,s) (https://play.golang.org/p/X9leJeIvxcA). Вызывая метод у nil из примера выше, мы практически вызываем функцию с nil в аргументах, а это уже вполне обычная ситуация. Поэтому в Go nil является корректным получателем для методов.

Так как получатель метода — это на самом деле аргумент, то рекомендации по использованию семантики «значение» или «указатель» для получателя метода аналогичны рекомендациям для аргументов функций. Они, в свою очередь, выводятся из базового правила Go: аргументы передаются в функцию всегда по значению. Это означает, что передача любого аргумента в функцию происходит через его копирование: если функция принимает на вход структуру, то внутрь нее придет полная копия этой структуры; если принимает указатель на объект, то придет новая переменная с указателем на этот же объект. Это видно, если сравнить адрес переменной до передачи в функцию с адресом аргумента внутри функции (https://play.golang.org/p/oc2ssC_Irs8, https://play.golang.org/p/FeQa2HUdX0a).

Когда используется передача по ссылке:

  • Для больших структур. Указатель занимает всего одно машинное слово (в зависимости от системы — 32, 64 бита). Поэтому при вызове метода с указателем в получателе копировать указатель дешевле, чем копировать весь объект, как это было бы в случае передачи по значению.
  • Если вызываемый метод изменяет данные самого объекта. При передаче получателя по ссылке метод может влиять на состояние вызывающего объекта путем косвенного внесения изменений. Что невозможно при передаче по значению.

Когда используется передача по значению:

  • Для простых встроенных типов, таких как числа, строки, bool. При использовании указателя задействуется практически такой же объем памяти, какой имеет сам объект этого типа, а стоимость его обслуживания сборщиком мусора (garbage collector) возрастает, как будет описано ниже.
  • Для слайсов, а также других ссылочных типов: мап и каналов — нет смысла брать указатель. Они сами по себе уже указатель.
  • При многопоточности передача по значению безопасна, в отличие от передачи по ссылке.
  • Для небольших структур. В таких случаях передача по значению происходит более эффективно. Это объясняется тем, что внутренние данные методов размещаются в отдельном фрейме стека. После выхода из функции ее фрейм очищается. Когда мы шарим что-то по указателю, мы переносим эти данные из стека в кучу, откуда эти данные могут быть доступны для других функций. Увеличение кучи создает дополнительную нагрузку на garbage collector, работа которого снижает скорость программы в среднем на 25 %. При использовании передачи по значению данные остаются в стеке и дополнительная работа сборщика мусора не требуется.

Когда нужно задуматься о семантике получателя:

  • Тип получателя может зависеть от предметной области. Билл Кеннеди в одном из выступлений привел хороший пример с типом user, описывающим пользователя. При передаче по значению для user будет создаваться копия. Это приведет к тому, что в программе одновременно может сосуществовать несколько копий одного и того же пользователя, которые далее могут независимо изменяться, что не соответствует предметной области, ведь реальный пользователь всегда один, и он не может в один и тот же момент описываться различными наборами данных.
  • Другим верным способом определить тип получателя для метода является метод-конструктор для его типа. Если конструктор возвращает значение/указатель, то уже при создании сущности подразумевается, что с ней далее будут работать как со значением/указателем. Поэтому в получателе методов тоже лучше использовать эту же семантику.
  • Существует неписаное правило, при нарушении которого компилятор не будет ругаться, но ваш код точно не станет от этого лучше. Если в одном из методов типа используется в качестве получателя указатель/значение, то для соблюдения консистентности и в остальных методах нужно использовать указатель/значение. В методах типа не должно быть мешанины из value- и pointer-получателей.

Что в итоге


В Go Value семантика означает копирование значения, pointer-семантика — предоставление доступа к значению. Это относится как к аргументам методов, так и к их получателям. Для встроенных типов, таких как числа, строки, слайсы, мапы, каналы, и небольших структур практически всегда нужно использовать передачу по значению. Для структур, занимающих большой объем памяти, и структур, состояние которых может косвенно изменяться их методами, нужно использовать передачу по ссылке. Также семантика получателя может зависеть от предметной области, которую описывает тип, семантики, возвращаемой в его фабрике, и уже используемой семантики получателя в других методах данного типа.
Теги:
Хабы:
Всего голосов 11: ↑10 и ↓1+9
Комментарии6

Публикации