Подтвердить что ты не робот

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

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

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

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

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

// 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 нс/оп)

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

4b9b3361

Ответ 1

Обратите внимание, что часто задаваемые вопросы указывают на согласованность

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

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

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

Сейчас:

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

Комментарий Комментарий к коду может помочь:

  • Если получатель - это карта, func или chan, не используйте указатель на нее.
  • Если ресивер является срезом, и метод не выполняет повторного выделения или перераспределения среза, не используйте указатель на него.
  • Если метод должен мутировать приемник, приемник должен быть указателем.
  • Если получатель представляет собой структуру, содержащую поле 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) {
...
}

Ответ 2

Добавлю дополнительно в @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-oriented-golang-4fa62b88c701