Указатели против значений в параметрах и возвращаемых значениях

330

В Go есть различные способы вернуть structзначение или его часть. Для отдельных я видел:

type MyStruct struct {
    Val int
}

func myfunc() MyStruct {
    return MyStruct{Val: 1}
}

func myfunc() *MyStruct {
    return &MyStruct{}
}

func myfunc(s *MyStruct) {
    s.Val = 1
}

Я понимаю разницу между ними. Первый возвращает копию структуры, второй - указатель на значение структуры, созданное в функции, третий ожидает, что существующая структура будет передана, и переопределяет значение.

Я видел, как все эти паттерны использовались в разных контекстах, мне интересно, каковы лучшие практики в этом отношении. Когда бы вы использовали какой? Например, первый может подойти для небольших структур (поскольку накладные расходы минимальны), второй для больших. И третье, если вы хотите максимально эффективно использовать память, потому что вы можете легко повторно использовать один экземпляр структуры между вызовами. Есть ли лучшие практики, когда использовать какие?

Аналогично, тот же вопрос относительно ломтиков:

func myfunc() []MyStruct {
    return []MyStruct{ MyStruct{Val: 1} }
}

func myfunc() []*MyStruct {
    return []MyStruct{ &MyStruct{Val: 1} }
}

func myfunc(s *[]MyStruct) {
    *s = []MyStruct{ MyStruct{Val: 1} }
}

func myfunc(s *[]*MyStruct) {
    *s = []MyStruct{ &MyStruct{Val: 1} }
}

Опять же: каковы лучшие практики здесь. Я знаю, что срезы - это всегда указатели, поэтому возвращать указатель на срез не нужно. Однако должен ли я возвращать фрагмент структурных значений, фрагмент указателей на структуры, должен ли я указывать указатель на фрагмент в качестве аргумента (шаблон, используемый в API Go App Engine )?

Зеф Хемел
источник
1
Как вы говорите, это действительно зависит от варианта использования. Все действительны в зависимости от ситуации - это изменчивый объект? мы хотим копию или указатель? и т.д. Кстати, вы не упомянули об использовании new(MyStruct):) Но на самом деле нет никакой разницы между различными методами распределения указателей и их возврата.
Not_a_Golfer
15
Это буквально закончилось на инженерном. Структуры должны быть довольно большими, чтобы возврат указателя делал вашу программу быстрее. Просто не беспокойтесь, код, профиль, исправить, если это полезно.
Фолькер
1
Существует только один способ вернуть значение или указатель, а именно: вернуть значение или указатель. Как вы их распределяете - это отдельный вопрос. Используйте то, что подходит для вашей ситуации, и начните писать код, прежде чем беспокоиться об этом.
JimB
3
Кстати, просто из любопытства я оценил это. Возвращение структур против указателей, кажется, примерно одинаковой скорости, но передача указателей на функции по линиям значительно быстрее. Хотя не на уровне это будет иметь значение
Not_a_Golfer
1
@ Not_a_Golfer: я бы предположил, что просто выделение bc выполняется вне функции. Кроме того, значения сравнения с указателями зависят от размера структуры и шаблонов доступа к памяти после факта. Копирование объектов размером с кэш-память выполняется настолько быстро, насколько вы можете, и скорость разыменования указателей из кэша ЦП значительно отличается от разыменования их из основной памяти.
JimB

Ответы:

392

тл; др :

  • Методы, использующие указатели приемника, распространены; Основное правило для получателей: «Если сомневаетесь, используйте указатель».
  • Срезы, карты, каналы, строки, значения функций и значения интерфейса реализованы с помощью указателей внутри, и указатель на них часто избыточен.
  • В другом месте используйте указатели для больших структур или структур, которые вам придется изменить, и в противном случае передайте значения , потому что получение неожиданных изменений с помощью указателя вводит в заблуждение.

Один случай, когда вы должны часто использовать указатель:

  • Получатели являются указателями чаще, чем другие аргументы. Нет ничего необычного в том, что методы модифицируют то, к чему они обращаются, или для именованных типов, чтобы быть большими структурами, поэтому рекомендуется указывать указатели по умолчанию, за исключением редких случаев.
    • Утилита copyfighter Джеффа Ходжеса автоматически ищет не крошечные получатели, переданные по значению.

Некоторые ситуации, когда вам не нужны указатели:

  • Рекомендации по проверке кода предлагают передавать в качестве значений небольшие структуры, например type Point struct { latitude, longitude float64 }, и, возможно, даже немного больше, если только вызываемая функция не должна иметь возможность изменять их на месте.

    • Семантика значений позволяет избежать наложения ситуаций, когда присваивание здесь изменяет значение неожиданно.
    • Нелегко жертвовать чистой семантикой ради небольшой скорости, и иногда передача небольших структур по значению на самом деле более эффективна, поскольку позволяет избежать пропусков кэша или выделения кучи.
    • Итак, страница комментариев к обзору кода в Go Wiki предлагает переходить по значению, когда структуры малы и, вероятно, останутся такими.
    • Если «большая» отсечка кажется расплывчатой, это так; возможно, многие структуры находятся в диапазоне, где указатель или значение в порядке. В качестве нижней границы комментарии к обзору кода предполагают, что срезы (три машинных слова) целесообразно использовать в качестве получателей значений. Как нечто ближе к верхней границе, bytes.Replaceзанимает 10 слов аргументов (три среза и an int).
  • Для слайсов вам не нужно передавать указатель для изменения элементов массива. io.Reader.Read(p []byte)изменяет байты p, например. Возможно, это особый случай «обработки небольших структур как значений», поскольку внутренне вы передаете небольшую структуру, называемую заголовком среза (см. Объяснение Russ Cox (rsc) ). Точно так же вам не нужен указатель, чтобы изменить карту или общаться на канале .

  • Для срезов вы изменяете (изменяете начало / длину / емкость) встроенные функции, такие как appendпринятие значения среза и возвращение нового. Я подражаю этому; избегая наложения псевдонимов, возврат нового среза помогает привлечь внимание к тому факту, что может быть выделен новый массив, и он знаком вызывающим.

    • Не всегда практично следовать этой схеме. Некоторые инструменты, такие как интерфейсы баз данных или сериализаторы, должны добавлять к фрагменту, тип которого неизвестен во время компиляции. Иногда они принимают указатель на фрагмент в interface{}параметре.
  • Карты, каналы, строки, а также значения функций и интерфейсов , например срезы, являются внутренними ссылками или структурами, которые уже содержат ссылки, поэтому, если вы просто пытаетесь избежать копирования базовых данных, вам не нужно передавать на них указатели , (RSC написал отдельный пост о том, как хранятся значения интерфейса ).

    • Вам все еще может понадобиться передать указатели в более редком случае, когда вы хотите изменить структуру вызывающей стороны: например, flag.StringVarпринимает *stringпо этой причине.

Где вы используете указатели:

  • Подумайте, должна ли ваша функция быть методом с той структурой, на которую вам нужен указатель. Люди ожидают множество методов xдля модификации x, поэтому создание измененной структуры получателя может помочь минимизировать удивление. Существуют рекомендации о том, когда приемники должны быть указателями.

  • Функции, которые влияют на их параметры не получателя, должны прояснить это в godoc или, еще лучше, в godoc и в названии (например reader.WriteTo(writer)).

  • Вы упоминаете принятие указателя, чтобы избежать выделения, разрешая повторное использование; изменение API для повторного использования памяти - это оптимизация, которую я буду откладывать до тех пор, пока не станет ясно, что выделения имеют нетривиальную стоимость, а затем я бы искал способ, который не навязывает более хитрый API всем пользователям:

    1. Для того, чтобы избежать выделения средств, анализ выхода Go - ваш друг. Иногда вы можете помочь ему избежать выделения кучи, создав типы, которые можно инициализировать с помощью тривиального конструктора, простого литерала или полезного нулевого значения, например bytes.Buffer.
    2. Рассмотрим Reset()метод возврата объекта в пустое состояние, как предлагают некоторые типы stdlib. Пользователи, которым все равно или они не могут сохранить выделение, не должны вызывать его.
    3. Для удобства рассмотрите возможность написания методов изменения на месте и функций создания с нуля как совпадающих пар: existingUser.LoadFromJSON(json []byte) errorих можно обернуть NewUserFromJSON(json []byte) (*User, error). Опять же, это подталкивает выбор между ленью и ограничением распределения для каждого звонящего.
    4. Вызывающие абоненты, пытающиеся утилизировать память, могут разрешить sync.Poolнекоторые детали. Если конкретное распределение создает большое давление памяти, вы уверены, что знаете, когда выделение больше не используется, и у вас нет лучшей оптимизации, sync.Poolможет помочь. (CloudFlare опубликовал полезную (предварительную sync.Pool) запись в блоге об утилизации.)

И, наконец, о том, должны ли ваши срезы иметь указатели: срезы значений могут быть полезны и сэкономить ваши выделения и потери в кеше. Там могут быть блокировщики:

  • API для создания ваших предметов может навязывать вам указатели, например, вам нужно вызывать, NewFoo() *Fooа не отпускать Go, инициализировать нулевым значением .
  • Желаемое время жизни предметов не может быть одинаковым. Весь кусок освобождается сразу; если 99% элементов больше не нужны, но у вас есть указатели на другие 1%, весь массив остается выделенным.
  • Перемещение предметов может вызвать проблемы. Примечательно, что appendкопирует элементы при увеличении базового массива . Указатели, которые вы получили до того, как appendуказали на неправильное место после, копирование может быть медленнее для огромных структур, например, sync.Mutexкопирование запрещено Вставка / удаление в середине и сортировка аналогичным образом перемещают элементы.

В общих чертах, значения срезов могут иметь смысл, если вы либо располагаете все свои элементы заранее и не перемещаете их (например, не больше appends после начальной настройки), либо если вы продолжаете перемещать их, но вы уверены, что это ОК (нет / осторожного использования указателей на элементы, элементы достаточно малы для эффективного копирования и т. Д.). Иногда вам нужно подумать или измерить специфику вашей ситуации, но это грубое руководство.

twotwotwo
источник
12
Что означает большие структуры? Есть ли пример большой структуры и небольшой структуры?
пользователь без шапки
1
Как вы скажете байты. Замена занимает 80 байтов аргументов на amd64?
Тим Ву
2
Подпись есть Replace(s, old, new []byte, n int) []byte; s, old и new - это три слова каждое ( заголовки слайса(ptr, len, cap) ) и n intодно слово, поэтому 10 слов, что при восьми байтах на слово составляет 80 байтов.
twotwotwo
6
Как вы определяете большие структуры? Насколько большой большой?
Энди Альдо
3
@AndyAldo Ни один из моих источников (комментарии к обзору кода и т. Д.) Не определяет пороговое значение, поэтому я решил сказать, что это суждение, а не пороговое значение. Три слова (например, фрагмент) довольно последовательно рассматриваются как подходящие для значения в stdlib. Я только что нашел экземпляр получателя значения из пяти слов (text / scanner.Position), но я не стал бы в него много читать (он также передается как указатель!). В отсутствие тестов и т. Д. Я бы просто делал все, что кажется наиболее удобным для удобства чтения.
twotwotwo
10

Три основные причины, по которым вы хотите использовать приемники методов в качестве указателей:

  1. «Во-первых, и это наиболее важно, должен ли метод модифицировать приемник? Если это так, получатель должен быть указателем».

  2. «Во-вторых, это вопрос эффективности. Если приемник большой, например, большая структура, будет гораздо дешевле использовать указатель приемника».

  3. «Далее следует согласованность. Если у некоторых методов типа должны быть указатели-получатели, то и у остальных тоже должно быть, поэтому набор методов согласован независимо от того, как используется тип»

Ссылка: https://golang.org/doc/faq#methods_on_values_or_pointers

Изменить: Другая важная вещь, чтобы знать фактический «тип», который вы отправляете в функцию. Типом может быть «тип значения» или «ссылочный тип».

Даже если срезы и карты действуют как ссылки, нам может потребоваться передать их как указатели в сценариях, таких как изменение длины среза в функции.

Сантош Пиллаи
источник
1
Для 2 какой отсечки? Как я узнаю, является ли моя структура большой или маленькой? Кроме того, существует ли структура, которая достаточно мала, чтобы более эффективно использовать значение, а не указатель (чтобы на него не нужно было ссылаться из кучи)?
злотника
Я бы сказал, что чем больше число полей и / или вложенных структур внутри, тем больше структура. Я не уверен, есть ли конкретное ограничение или стандартный способ узнать, когда структуру можно назвать «большой» или «большой». Если я использую или создаю структуру, я бы знал, большая она или маленькая, основываясь на том, что я сказал выше. Но это только я!
Сантош Пиллай
2

Случай, когда вам, как правило, нужно возвращать указатель, - это когда создается экземпляр некоторого ресурса или ресурса общего доступа . Это часто делается функциями с префиксом New.

Поскольку они представляют конкретный экземпляр чего-либо и им может потребоваться координировать какую-либо деятельность, не имеет большого смысла генерировать дублированные / копируемые структуры, представляющие один и тот же ресурс - поэтому возвращаемый указатель действует как дескриптор самого ресурса ,

Некоторые примеры:

В других случаях указатели возвращаются только потому, что структура может быть слишком большой для копирования по умолчанию:


В качестве альтернативы, непосредственного возврата указателей можно избежать, вместо этого возвращая копию структуры, которая содержит указатель внутри, но, возможно, это не считается идиоматическим:

nobar
источник
В этом анализе подразумевается, что по умолчанию структуры копируются по значению (но не обязательно их косвенные члены).
nobar
2

Если вы можете (например, не общий ресурс, который не нужно передавать в качестве ссылки), используйте значение. По следующим причинам:

  1. Ваш код будет более приятным и читаемым, избегая операторов указателей и нулевых проверок.
  2. Ваш код будет безопаснее от паники Null Pointer.
  3. Ваш код будет часто быстрее: да, быстрее! Зачем?

Причина 1 : вы будете размещать меньше предметов в стеке. Выделение / освобождение из стека происходит немедленно, но выделение / освобождение в куче может быть очень дорогим (время выделения + сборка мусора). Вы можете увидеть некоторые основные цифры здесь: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Причина 2 : особенно если вы храните возвращаемые значения в срезах, ваши объекты памяти будут более компактными в памяти: циклическое выполнение среза, в котором все элементы являются смежными, намного быстрее, чем итерирование среза, в котором все элементы являются указателями на другие части памяти , Не для шага косвенности, а для увеличения промахов кеша.

Разрушитель мифов : типичная строка кэша x86 занимает 64 байта. Большинство структур меньше, чем это. Время копирования строки кэша в памяти аналогично копированию указателя.

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

Марио
источник