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

В 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} }
}

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

Ответ 1

тл; др:

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

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

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

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

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

    • Семантика значений позволяет избежать наложения псевдонимов, когда присваивание здесь изменяет значение неожиданно.
    • Go-y не жертвует чистой семантикой ради небольшой скорости, и иногда передача небольших структур по значению на самом деле более эффективна, поскольку позволяет избежать пропусков кэша или выделения кучи.
    • Итак, страница комментариев к обзору кода предлагает переходить по значению, когда структуры малы и, вероятно, останутся такими.
    • Если "большая" отсечка кажется расплывчатой, это так; возможно, многие структуры находятся в диапазоне, где указатель или значение в порядке. В качестве нижней границы комментарии к обзору кода предполагают, что срезы (три машинных слова) целесообразно использовать в качестве получателей значений. Как что-то ближе к верхней границе, bytes.Replace берет аргументы на 10 слов (три среза и 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 escape анализ - ваш друг. Иногда вы можете помочь ему избежать выделения кучи, создав типы, которые можно инициализировать с помощью тривиального конструктора, простого литерала или полезного нулевого значения, например bytes.Buffer.
    2. Рассмотрим метод Reset(), чтобы вернуть объект в пустое состояние, как предлагают некоторые типы stdlib. Пользователи, которым все равно или они не могут сохранить выделение, не должны вызывать его.
    3. Для удобства рассмотрим написание методов изменения на месте и функций создания с нуля в качестве совпадающих пар: existingUser.LoadFromJSON(json []byte) error может быть заключен в NewUserFromJSON(json []byte) (*User, error). Опять же, это подталкивает выбор между ленью и ограничением выделения для отдельного абонента.
    4. Вызывающие абоненты, пытающиеся утилизировать память, могут sync.Pool обрабатывать некоторые детали. Если конкретное выделение создает большое давление памяти, вы уверены, что знаете, когда выделение больше не используется, и у вас нет лучшей оптимизации, sync.Pool может помочь. (CloudFlare опубликовал полезный (pre- sync.Pool) пост в блоге об утилизации.)

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

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

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

Ответ 2

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

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

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

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

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

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

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

Ответ 3

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

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

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

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


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