webentwicklung-frage-antwort-db.com.de

Wertempfänger vs. Zeigerempfänger

Es ist für mich sehr unklar, in welchem ​​Fall ich einen Wertempfänger verwenden möchte, anstatt immer einen Zeigerempfänger zu verwenden.
Um es aus den Dokumenten zusammenzufassen:

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

Das docs sagt auch: "Für Typen wie Basistypen, Slices und kleine Strukturen ist ein Wertempfänger sehr billig, es sei denn, die Semantik der Methode erfordert dies ein Zeiger, ein Wertempfänger ist effizient und klar. "

Der erste Punkt besagt, dass es "sehr billig" ist, aber die Frage ist mehr, ob es billiger ist als der Zeigerempfänger. Also habe ich einen kleinen Benchmark erstellt (code on Gist) , der mir zeigte, dass der Zeigerempfänger auch für eine Struktur mit nur einem Zeichenfolgenfeld schneller ist. Das sind die Ergebnisse:

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

(Bearbeiten: Bitte beachten Sie, dass der zweite Punkt in neueren go-Versionen ungültig wurde, siehe Kommentare).
Der zweite Punkt besagt, dass es "effizient und klar" ist, was mehr eine Geschmackssache ist, nicht wahr? Persönlich bevorzuge ich Konsistenz, indem ich überall den gleichen Weg benutze. Effizienz in welchem ​​Sinne? In Bezug auf die Leistung scheint es, dass Zeiger fast immer effizienter sind. Wenige Testläufe mit einer int-Eigenschaft zeigten einen minimalen Vorteil des Value-Empfängers (Bereich von 0,01 bis 0,1 ns/op).

Kann mir jemand einen Fall nennen, in dem ein Wertempfänger eindeutig sinnvoller ist als ein Zeigerempfänger? Oder mache ich im Benchmark etwas falsch, habe ich andere Faktoren übersehen?

83
Chrisport

Beachten Sie, dass in FAQ wird die Konsistenz erwähnt

Weiter ist Konsistenz. Wenn einige der Methoden des Typs Zeigerempfänger haben müssen, sollte dies auch der Rest sein, damit der Methodensatz unabhängig von der Verwendung des Typs konsistent ist. Einzelheiten finden Sie in Abschnitt über Methodensätze s.

Wie erwähnt in diesem Thread :

Die Regel für Zeiger im Vergleich zu Werten für Empfänger lautet, dass Wertemethoden für Zeiger und Werte aufgerufen werden können, Zeigermethoden jedoch nur für Zeiger

Jetzt:

Kann mir jemand einen Fall nennen, in dem ein Wertempfänger eindeutig sinnvoller ist als ein Zeigerempfänger?

Das Code Review Kommentar kann helfen:

  • Wenn es sich bei dem Empfänger um eine Karte, eine Funk oder einen Kanal handelt, verwenden Sie keinen Zeiger darauf.
  • Wenn der Empfänger ein Slice ist und die Methode das Slice nicht neu verschiebt oder zuordnet, verwenden Sie keinen Zeiger darauf.
  • Wenn die Methode den Empfänger mutieren muss, muss der Empfänger ein Zeiger sein.
  • Wenn der Empfänger eine Struktur ist, die ein sync.Mutex Oder ein ähnliches Synchronisierungsfeld enthält, muss der Empfänger ein Zeiger sein, um ein Kopieren zu vermeiden.
  • Wenn der Empfänger eine große Struktur oder ein großes Array ist, ist ein Zeigerempfänger effizienter. Wie groß ist groß? Angenommen, es entspricht der Übergabe aller Elemente als Argumente an die Methode. Wenn sich das zu groß anfühlt, ist es auch zu groß für den Empfänger.
  • Können Funktionen oder Methoden gleichzeitig oder beim Aufruf dieser Methode den Empfänger mutieren? Ein Wertetyp erstellt beim Aufrufen der Methode eine Kopie des Empfängers, sodass keine externen Aktualisierungen auf diesen Empfänger angewendet werden. Wenn Änderungen im ursprünglichen Empfänger sichtbar sein müssen, muss der Empfänger ein Zeiger sein.
  • Wenn der Empfänger eine Struktur, ein Array oder ein Slice ist und eines seiner Elemente ein Zeiger auf etwas ist, das möglicherweise mutiert, bevorzugen Sie einen Zeigerempfänger, da dies dem Leser die Absicht klarer macht.
  • Wenn der Empfänger ein kleines Array oder eine kleine Struktur ist, bei der es sich natürlich um einen Werttyp handelt (zum Beispiel so etwas wie der Typ time.Time), Mit Keine veränderlichen Felder und keine Zeiger oder nur ein einfacher Basistyp wie int oder string. Ein Wertempfänger ist sinnvoll .
    Ein Wertempfänger kann die Menge an Müll reduzieren, die erzeugt werden kann. Wenn ein Wert an eine value-Methode übergeben wird, kann eine On-Stack-Kopie verwendet werden, anstatt ihn auf dem Heap zuzuweisen. Dies ist nicht immer erfolgreich.) Wählen Sie aus diesem Grund keinen Wertempfängertyp, ohne zuvor ein Profil zu erstellen.
  • Verwenden Sie im Zweifelsfall einen Zeigerempfänger.

Der fettgedruckte Teil befindet sich beispielsweise in 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) {
...
}
101
VonC

Ergänzend zu @VonC tolle, aussagekräftige Antwort hinzufügen.

Ich bin überrascht, dass niemand die Wartungskosten wirklich erwähnte, sobald das Projekt größer wird, alte Entwickler gehen und neue kommen. Go ist sicher eine junge Sprache.

Im Allgemeinen versuche ich, Hinweise zu vermeiden, wenn ich kann, aber sie haben ihren Platz und ihre Schönheit.

Ich benutze Zeiger, wenn:

  • arbeiten mit großen Datensätzen
  • einen strukturerhaltenden Zustand haben, z.B. TokenCache,
    • Ich stelle sicher, dass ALLE Felder PRIVAT sind, eine Interaktion ist nur über definierte Methodenempfänger möglich
    • Ich gebe diese Funktion an keine Goroutine weiter

Z.B:

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
}

Gründe, warum ich Hinweise vermeide:

  • zeiger sind nicht gleichzeitig sicher (der ganze Punkt von GoLang)
  • einmal Zeigerempfänger, immer Zeigerempfänger (für alle Struct-Konsistenzmethoden)
  • mutexe sind sicherlich teurer, langsamer und schwerer zu pflegen als die "Wertkopiekosten".
  • apropos "Wertkopiekosten", ist das wirklich ein Problem? Vorzeitige Optimierung ist die Wurzel allen Übels. Sie können später immer noch Zeiger hinzufügen
  • es zwingt mich direkt, bewusst kleine Strukturen zu entwerfen
  • zeiger können größtenteils vermieden werden, indem reine Funktionen mit klarer Absicht und offensichtlicher E/A entworfen werden
  • müllsammlung ist schwieriger mit Zeigern, glaube ich
  • leichter über Kapselung, Verantwortlichkeiten zu streiten
  • halte es einfach, dumm (ja, Zeiger können schwierig sein, weil du nie den Entwickler des nächsten Projekts kennst)
  • unit Testing ist wie ein Spaziergang durch den rosaroten Garten (slowakischer Ausdruck?), bedeutet einfach
  • no NIL if conditions (NIL kann dort übergeben werden, wo ein Zeiger erwartet wurde)

Meine Faustregel: Schreiben Sie so viele gekapselte Methoden wie möglich wie:

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) 

UPDATE:

Diese Frage hat mich dazu inspiriert, das Thema genauer zu untersuchen und einen Blog-Beitrag darüber zu schreiben https://medium.com/gophersland/Gopher-vs-object-oriented-golang-4fa62b88c701

11
BlocksByLukas