Приемник значения против приемника указателя

108

Для меня очень непонятно, в каком случае я хотел бы использовать приемник значения вместо того, чтобы всегда использовать приемник указателя.
Резюмируя из документов:

type T struct {
    a int
}
func (tv  T) Mv(a int) int         { return 0 }  // value receiver
func (tp *T) Mp(f float32) float32 { return 1 }  // pointer receiver

В документации также говорится: «Для таких типов, как базовые типы, срезы и небольшие структуры, приемник значения очень дешев, поэтому, если семантика метода не требует указателя, приемник значения эффективен и понятен».

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

// Struct one empty string property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  500000000                3.62 ns/op


// Struct one zero int property
BenchmarkChangePointerReceiver  2000000000               0.36 ns/op
BenchmarkChangeItValueReceiver  2000000000               0.36 ns/op

(Изменить: обратите внимание, что второй пункт стал недействительным в более новых версиях go, см. Комментарии) .
Во-вторых, он говорит, что это «эффективно и ясно», что больше вопрос вкуса, не так ли? Лично я предпочитаю последовательность, используя везде одинаково. Эффективность в каком смысле? с точки зрения производительности кажется, что указатель почти всегда более эффективен. Несколько тестовых прогонов с одним свойством int показали минимальное преимущество приемника Value (диапазон 0,01-0,1 нс / операцию)

Может ли кто-нибудь сказать мне случай, когда приемник значения явно имеет больше смысла, чем приемник указателя? Или я что-то не так делаю в тесте, не учел другие факторы?

Chrisport
источник
3
Я провел аналогичные тесты с одним строковым полем, а также с двумя полями: строковое и целочисленное. Я получил более быстрые результаты от получателя значений. BenchmarkChangePointerReceiver-4 10000000000 0,99 нс / операцию BenchmarkChangeItValueReceiver-4 10000000000 0,33 нс / операцию Используется Go 1.8. Интересно, производилась ли оптимизация компилятора с тех пор, как вы в последний раз запускали тесты. Смотрите суть для более подробной информации.
pbitty 08
2
Ты прав. Запуская свой исходный тест с использованием Go1.9, теперь я также получаю другие результаты. Приемник указателя 0,60 нс / операцию, приемник значения 0,38 нс /
операцию

Ответы:

119

Обратите внимание, что в FAQ упоминается согласованность

Далее следует последовательность. Если некоторые из методов типа должны иметь приемники указателей, остальные должны также иметь, поэтому набор методов согласован независимо от того, как используется тип. См. Подробности в разделе о наборах методов .

Как упоминалось в этой теме :

Правило относительно указателей и значений для приемников состоит в том, что методы значений могут быть вызваны для указателей и значений, но методы указателей могут быть вызваны только для указателей.

Сейчас:

Может ли кто-нибудь сказать мне случай, когда приемник значения явно имеет больше смысла, чем приемник указателя?

Комментарий Код Обзор может помочь:

  • Если получателем является карта, функция или канал, не используйте указатель на нее.
  • Если получатель является срезом, а метод не выполняет повторное срезание или перераспределение среза, не используйте указатель на него.
  • Если методу необходимо изменить получателя, получатель должен быть указателем.
  • Если получатель - это структура, которая содержит такое sync.Mutexили подобное поле синхронизации, получатель должен быть указателем, чтобы избежать копирования.
  • Если получателем является большая структура или массив, получатель указателя более эффективен. Насколько большой большой? Предположим, что это эквивалентно передаче всех его элементов в качестве аргументов методу. Если он кажется слишком большим, он также слишком велик для приемника.
  • Может ли функция или методы, одновременно или при вызове из этого метода, изменять получателя? Тип значения создает копию получателя при вызове метода, поэтому внешние обновления не будут применяться к этому получателю. Если изменения должны быть видны в исходном получателе, получатель должен быть указателем.
  • Если получатель является структурой, массивом или фрагментом, а любой из его элементов является указателем на что-то, что может изменяться, предпочтите получателя указателя, так как это сделает намерение более понятным для читателя.
  • Если получатель представляет собой небольшой массив или структуру, которая, естественно, является типом значения (например, чем-то вроде time.Timeтипа), без изменяемых полей и указателей, или представляет собой простой базовый тип, такой как int или string, получатель значения делает смысл .
    Получатель значения может уменьшить количество создаваемого мусора; если значение передается методу значения, можно использовать копию в стеке вместо выделения в куче. (Компилятор старается избегать этого распределения, но это не всегда удается.) По этой причине не выбирайте тип получателя значения без предварительного профилирования.
  • Наконец, в случае сомнений используйте приемник указателя.

Часть, выделенная жирным шрифтом, находится, например, в net/http/server.go#Write():

// Write writes the headers described in h to w.
//
// This method has a value receiver, despite the somewhat large size
// of h, because it prevents an allocation. The escape analysis isn't
// smart enough to realize this function doesn't mutate h.
func (h extraHeader) Write(w *bufio.Writer) {
...
}
VonC
источник
16
The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers На самом деле неправда. Методы приемника значения и приемника указателя могут быть вызваны для правильно набранного указателя или не указателя. Независимо от того, для чего вызывается метод, в теле метода идентификатор получателя ссылается на значение копии, когда используется получатель значения, и указатель, когда используется получатель указателя: см. Play.golang.org/p / 3WHGaAbURM
Харт Симха
3
Существует большое объяснение здесь «Если х адресацией и & присваиватель иксы содержит т, хт () является сокращением (& х) .m ().»
тера
@tera Да: это обсуждается на stackoverflow.com/q/43953187/6309
VonC
4
Отличный ответ, но я категорически не согласен с этим: «поскольку это сделает намерение более ясным», NOPE, чистый API, X как аргумент и Y как возвращаемое значение - явное намерение. Передача Struct с помощью указателя и тратить время на внимательное чтение кода, чтобы проверить, какие все атрибуты изменяются, далеко не ясно и не обслуживается.
Лукас Лукач
@HartSimha Я думаю, что сообщение выше указывает на то, что методы приемника указателя не входят в «набор методов» для типов значений. В связанном детской площадке, добавив следующую строку приведет к ошибке компиляции: Int(5).increment_by_one_ptr(). Точно так же признак, определяющий метод increment_by_one_ptr, не будет удовлетворен значением типа Int.
Гаурав Агарвал,
16

Чтобы добавить в @VonC отличный информативный ответ.

Я удивлен, что никто на самом деле не упомянул стоимость обслуживания, когда проект разрастется, старые разработчики уйдут и появятся новые. Го, безусловно, молодой язык.

Вообще говоря, я стараюсь избегать указателей, когда могу, но они имеют свое место и красоту.

Я использую указатели, когда:

  • работа с большими наборами данных
  • иметь состояние поддержки структуры, например TokenCache,
    • Я удостоверяюсь, что ВСЕ поля ЧАСТНЫЕ, взаимодействие возможно только через определенные приемники метода
    • Я не передаю эту функцию ни одной горутине

Например:

type TokenCache struct {
    cache map[string]map[string]bool
}

func (c *TokenCache) Add(contract string, token string, authorized bool) {
    tokens := c.cache[contract]
    if tokens == nil {
        tokens = make(map[string]bool)
    }

    tokens[token] = authorized
    c.cache[contract] = tokens
}

Причины, по которым я избегаю указателей:

  • указатели одновременно небезопасны (весь смысл GoLang)
  • один раз получатель указателя, всегда получатель указателя (для всех методов Struct для согласованности)
  • мьютексы, безусловно, дороже, медленнее и сложнее в обслуживании по сравнению со "стоимостью копирования значения"
  • говоря о «стоимости копий», это действительно проблема? Преждевременная оптимизация - корень всех зол, вы всегда можете добавить указатели позже
  • это прямо, сознательно заставляет меня разрабатывать небольшие структуры
  • указателей можно в основном избежать, создавая чистые функции с четким намерением и очевидным вводом-выводом.
  • Я считаю, что сборка мусора сложнее с указателями
  • легче спорить об инкапсуляции, ответственности
  • будь простым, глупым (да, указатели могут быть сложными, потому что никогда не знаешь разработчика следующего проекта)
  • модульное тестирование похоже на прогулку по розовому саду (только на словацком языке?), означает легкий
  • нет NIL, если условия (NIL может быть передан там, где ожидался указатель)

Мое эмпирическое правило - напишите как можно больше инкапсулированных методов, например:

package rsa

// EncryptPKCS1v15 encrypts the given message with RSA and the padding scheme from PKCS#1 v1.5.
func EncryptPKCS1v15(rand io.Reader, pub *PublicKey, msg []byte) ([]byte, error) {
    return []byte("secret text"), nil
}

cipherText, err := rsa.EncryptPKCS1v15(rand, pub, keyBlock) 

ОБНОВИТЬ:

Этот вопрос вдохновил меня на дальнейшее изучение темы и написать об этом сообщение в блоге https://medium.com/gophersland/gopher-vs-object-oriated-golang-4fa62b88c701

Лукас Лукач
источник
Мне нравится 99% того, что вы здесь говорите, и я полностью с этим согласен. Тем не менее, мне интересно, является ли ваш пример лучшим способом проиллюстрировать вашу точку зрения. Разве TokenCache по сути не является картой (от @VonC - «если получатель является картой, функцией или каналом, не используйте указатель на нее»). Поскольку карты являются ссылочными типами, что вы получите, сделав "Add ()" приемником указателя? Любые копии TokenCache будут ссылаться на одну и ту же карту. Смотрите эту игровую площадку для игры в Go - play.golang.com/p/Xda1rsGwvhq
Rich
Рад, что мы согласны. Отличная точка. На самом деле, я думаю, что использовал указатель в этом примере, потому что я скопировал его из проекта, в котором TokenCache обрабатывает больше вещей, чем только эта карта. И если я использую указатель в одном методе, я использую его во всех. Вы предлагаете удалить указатель из этого конкретного примера SO?
Лукас Лукач
LOL, копирование / вставка ударов снова! 😉 IMO, вы можете оставить его как есть, поскольку он иллюстрирует ловушку, в которую легко попасть, или вы можете заменить карту чем-то (-ми), демонстрирующим состояние и / или большую структуру данных.
Rich
Что ж, я уверен, что они прочитают комментарии ... PS: Богатый, ваши аргументы кажутся разумными, добавьте меня в LinkedIn (ссылка в моем профиле) с радостью подключусь.
Лукас Лукач