532 Stimmen

Pointers vs. Werte in Parametern und Rückgabewerten

In Go gibt es verschiedene Möglichkeiten, einen struct-Wert oder einen Teil davon zurückzugeben. Für einzelne habe ich gesehen:

type MyStruct struct {
    Val int
}

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

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

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

Ich verstehe die Unterschiede zwischen diesen. Der erste gibt eine Kopie des Structs zurück, der zweite einen Zeiger auf den innerhalb der Funktion erstellten Struct-Wert, der dritte erwartet einen vorhandenen Struct, der übergeben wird und den Wert überschreibt.

Ich habe all diese Muster in verschiedenen Kontexten gesehen und frage mich, welche bewährten Verfahren es in Bezug darauf gibt. Wann würden Sie welche verwenden? Zum Beispiel könnte der erste für kleine Strukturen in Ordnung sein (weil der Overhead minimal ist), der zweite für größere. Und der dritte, wenn Sie extrem speichereffizient sein möchten, weil Sie eine einzige Struct-Instanz zwischen den Aufrufen problemlos wiederverwenden können. Gibt es bewährte Verfahren für den Einsatz von welchem?

Ebenso die gleiche Frage in Bezug auf Slices:

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

Nochmals: Was sind hier bewährte Verfahren? Ich weiß, dass Slices immer Zeiger sind, daher ist es nicht sinnvoll, einen Zeiger auf einen Slice zurückzugeben. Sollte ich jedoch ein Slice von Struct-Werten zurückgeben, ein Slice von Zeigern auf Structs, sollte ich einen Zeiger auf einen Slice als Argument übergeben (ein Muster, das in der Go App Engine API) verwendet wird?

34voto

Mario Punkte 1515

Wenn Sie können (z. B. ein nicht gemeinsam genutztes Ressource, die nicht als Referenz übergeben werden muss), verwenden Sie einen Wert. Aus den folgenden Gründen:

  1. Ihr Code wird schöner und lesbarer, wobei Zeigeroperatoren und Nullüberprüfungen vermieden werden.
  2. Ihr Code wird sicherer gegen Null-Pointer-Panics sein.
  3. Ihr Code wird oft schneller sein: ja, schneller! Warum?

Grund 1: Sie werden weniger Elemente auf dem Heap zuweisen. Zuweisung/Freigabe vom Stapel erfolgt sofort, aber Zuweisung/Freigabe auf dem Heap kann sehr teuer sein (Zuweisungszeit + Garbage Collection). Sie können hier einige grundlegende Zahlen sehen: http://www.macias.info/entry/201802102230_go_values_vs_references.md

Grund 2: Insbesondere wenn Sie zurückgegebene Werte in Slices speichern, werden Ihre Speicherobjekte im Speicher kompakter sein: Das Schleifen durch einen Slice, bei dem alle Elemente zusammenhängend sind, ist viel schneller als das Iterieren durch einen Slice, bei dem alle Elemente Zeiger auf andere Teile des Speichers sind. Nicht für den Indirektionsschritt, sondern für die Zunahme von Cache-Miss-Raten.

Mythenbrecher: Eine typische x86-Cache-Line besteht aus 64 Bytes. Die meisten Strukturen sind kleiner als das. Die Zeit für das Kopieren einer Cache-Line im Speicher ist ähnlich wie das Kopieren eines Zeigers.

Nur wenn ein kritischer Teil Ihres Codes langsam ist, würde ich einige Mikro-Optimierungen versuchen und überprüfen, ob die Verwendung von Zeigern die Geschwindigkeit etwas verbessert, auf Kosten von weniger Lesbarkeit und Wartbarkeit.

22voto

Santosh Pillai Punkte 7203

Drei Hauptgründe, wann Sie Methodenempfänger als Zeiger verwenden möchten:

  1. "Erstens und am wichtigsten: Muss die Methode den Empfänger ändern? Wenn ja, muss der Empfänger ein Zeiger sein."

  2. "Zweitens spielt die Effizienz eine Rolle. Ist der Empfänger groß, zum Beispiel ein großes Struct, ist es viel günstiger, einen Zeigerempfänger zu verwenden."

  3. "Als Nächstes kommt die Konsistenz. Wenn einige der Methoden des Typs Zeigerempfänger haben müssen, sollten auch die anderen, damit der Methodensatz unabhängig davon, wie der Typ verwendet wird, konsistent ist."

Referenz : https://golang.org/doc/faq#methods_on_values_or_pointers

Bearbeiten : Eine weitere wichtige Sache ist zu wissen, welchen tatsächlichen "Typ" Sie an die Funktion senden. Der Typ kann entweder ein 'Werttyp' oder ein 'Referenztyp' sein.

Auch wenn Slices und Maps als Referenzen fungieren, möchten wir sie möglicherweise als Zeiger übergeben, z.B. wenn die Länge des Slices in der Funktion geändert wird.

9voto

Brent Bradburn Punkte 45995

Ein Fall, in dem Sie in der Regel einen Zeiger zurückgeben müssen, ist beim Konstruieren einer Instanz eines zustandsbehafteten oder teilbaren Ressource. Dies wird oft von Funktionen durchgeführt, die mit New beginnen.

Da sie eine spezifische Instanz von etwas darstellen und möglicherweise einige Aktivitäten koordinieren müssen, macht es nicht viel Sinn, duplizierte/kopierte Strukturen zu generieren, die dieselbe Ressource darstellen -- daher dient der zurückgegebene Zeiger als Handle zur Ressource selbst.

Einige Beispiele:

In anderen Fällen werden Zeiger zurückgegeben, nur weil die Struktur standardmäßig zu groß sein kann, um kopiert zu werden:


Alternativ könnten Zeiger direkt vermieden werden, indem stattdessen eine Kopie einer Struktur zurückgegeben wird, die den Zeiger intern enthält, aber vielleicht wird dies nicht als idiomatisch betrachtet:

3voto

vcycyv Punkte 538

Im Hinblick auf die Rückgabe von Strukturen vs. Zeigern war ich nach dem Lesen vieler hoch bewerteter Open-Source-Projekte auf GitHub verwirrt, da es viele Beispiele für beide Fälle gibt, bis ich diesen erstaunlichen Artikel gefunden habe: https://www.ardanlabs.com/blog/2014/12/using-pointers-in-go.html

"Im Allgemeinen teilen Sie Strukturtypwerte mit einem Zeiger, es sei denn, der Strukturtyp wurde implementiert, um sich wie ein primitives Datenwert zu verhalten.

Wenn Sie immer noch nicht sicher sind, ist dies eine andere Möglichkeit, darüber nachzudenken. Denken Sie bei jeder Struktur an eine Art von Natur. Wenn die Natur der Struktur etwas ist, das nicht verändert werden sollte, wie z.B. eine Zeit, eine Farbe oder eine Koordinate, dann implementieren Sie die Struktur als primitive Datenwert. Wenn die Natur der Struktur etwas ist, das sich ändern kann, auch wenn es in Ihrem Programm nie geschieht, handelt es sich nicht um einen primitiven Datenwert und sollte gemeinsam mit einem Zeiger implementiert werden. Erstellen Sie keine Strukturen, die eine Dualität der Natur haben."

Vollkommen überzeugt.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X