Odbiornik wartości a odbiornik wskaźnika

Jest dla mnie bardzo niejasne, w którym przypadku chciałbym użyć odbiornika wartości zamiast zawsze używać odbiornika wskaźnika.
Podsumowanie z docs:

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

Docs mówi również: "dla typów, takich jak typy podstawowe, plasterki i małe struktury, odbiornik wartości jest bardzo tani, więc jeśli semantyka metody nie wymaga wskaźnika, odbiornik wartości jest wydajny i przejrzysty."

Pierwszy punkt mówi, że jest "bardzo tani" , ale pytanie brzmi bardziej czy jest to tańsze niż odbiornik wskaźnika. Zrobiłem więc mały benchmark (code on gist), który pokazał mi, że odbiornik wskaźnika jest szybszy nawet dla struktury, która ma tylko jedno pole string. Oto wyniki:

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

(Edit: proszę zauważyć, że drugi punkt stał się nieważny w nowszych wersjach go, patrz komentarze) .
drugi punkt mówi, że jest "wydajny i przejrzysty", co jest bardziej kwestią gustu, prawda? Osobiście wolę spójność, używając wszędzie tego samego sposób. Efektywność w jakim sensie? wydajność wydaje się, że wskaźniki są prawie zawsze bardziej wydajne. Kilka testów z jedną właściwością int wykazało minimalną przewagę odbiornika wartości (Zakres 0.01-0.1 ns/op)

Czy ktoś może mi powiedzieć przypadek, w którym odbiornik wartości wyraźnie ma większy sens niż odbiornik wskaźnika? A może robię coś złego w benchmarku, czy przeoczyłem inne czynniki?

Author: Flimzy, 2015-01-05

2 answers

Zauważ, że FAQ wspomina o spójności

Następna jest spójność. Jeśli niektóre metody tego typu muszą mieć odbiorniki wskaźników, reszta też powinna, więc zestaw metod jest spójny niezależnie od tego, w jaki sposób typ jest używany. Szczegóły można znaleźć w sekcji dotyczącej zestawu metod s.

Jak wspomniano w tym wątku :

Reguła o wskaźnikach a wartościach dla odbiorników jest taka, że metody wartości mogą być wywoływane na wskaźnikach i wartościach, ale metody wskaźników mogą być wywoływane tylko on pointers

Teraz:

Czy ktoś może mi powiedzieć przypadek, w którym odbiornik wartości wyraźnie ma większy sens niż odbiornik wskaźnika?

Komentarz do kodu może pomóc:

  • jeśli odbiornik jest mapą, func lub chan, nie używaj do niej wskaźnika.
  • jeśli odbiornik jest plasterkiem, a metoda nie przywraca ani nie przydziela plasterka, nie używaj do niego wskaźnika.
  • jeśli metoda musi mutować odbiornik, odbiornik musi być wskaźnikiem.
  • jeśli odbiornik jest strukturą zawierającą sync.Mutex lub podobne pole synchronizacji, odbiornik musi być wskaźnikiem, aby uniknąć kopiowania.
  • jeśli odbiornik jest dużą strukturą lub tablicą, odbiornik wskaźnika jest bardziej wydajny. Jak duży jest duży? Załóżmy, że jest to równoważne przekazaniu wszystkich jej elementów jako argumentów do metody. Jeśli wydaje się to zbyt duże, jest również zbyt duże dla odbiornika.
  • może funkcjonować czy metody, albo jednocześnie lub gdy są wywoływane z tej metody, mutują odbiornik? Typ wartości tworzy kopię odbiornika podczas wywoływania metody, więc zewnętrzne aktualizacje nie będą stosowane do tego odbiornika. Jeśli zmiany muszą być widoczne w oryginalnym odbiorniku, odbiornik musi być wskaźnikiem.
  • jeśli odbiornik jest strukturą, tablicą lub slice, a którykolwiek z jego elementów jest wskaźnikiem do czegoś, co może mutować, preferuj odbiornik wskaźnika, ponieważ sprawi, że intencja będzie bardziej jasne dla czytelnika.
  • jeśli odbiornik jest małą tablicą lub strukturą, która jest naturalnie typem wartości (na przykład coś w rodzaju typu time.Time), bez zmiennych pól i bez wskaźników, lub jest tylko prostym typem podstawowym, takim jak int lub string, odbiornik wartości ma sens.
    odbiornik wartości może zmniejszyć ilość śmieci, które mogą być generowane; jeśli wartość jest przekazywana do metody wartości, można użyć kopii na stosie zamiast alokacji na stercie. (Kompilator stara się być inteligentny w unikaniu tej alokacji, ale nie zawsze może się to udać.) Z tego powodu nie należy wybierać typu odbiornika wartości bez uprzedniego profilowania.
  • wreszcie, w razie wątpliwości, użyj odbiornika wskaźnika.

Część pogrubiona znajduje się na przykład w 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) {
...
}
 136
Author: VonC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-01-05 08:10:01

Do dodania dodatkowo @VonC świetna, pouczająca odpowiedź.

Jestem zaskoczony, że nikt tak naprawdę nie wspomniał o kosztach utrzymania, gdy projekt się powiększy, odejdą Starzy deweloperzy i pojawi się nowy. Go z pewnością jest młodym językiem. Ogólnie rzecz biorąc, staram się unikać wskazówek, kiedy mogę, ale mają swoje miejsce i piękno.

Używam pointerów gdy:

  • praca z dużymi zbiorami danych
  • mają strukturę utrzymującą stan, np. TokenCache,
    • upewniam się, że wszystkie pola są prywatne, interakcja jest możliwa tylko za pomocą zdefiniowanej metody
    • nie przekazuję tej funkcji żadnemu goroutine

Np:

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
}

Powody, dla których unikam wskazówek:

  • wskaźniki nie są jednocześnie bezpieczne (cały punkt Golanga)
  • once pointer receiver, always pointer receiver (for all STRUCT ' s methods for konsystencja)
  • Mutexy są z pewnością droższe, wolniejsze i trudniejsze w utrzymaniu w porównaniu do"kosztu kopiowania wartości" [14]} Mówiąc o "wartościowym koszcie kopiowania", czy to naprawdę problem? Przedwczesna optymalizacja jest źródłem wszelkiego zła, zawsze możesz dodać wskaźniki później
  • to wprost, concierge zmusza mnie do projektowania małych struktur
  • wskaźniki można uniknąć, projektując czyste funkcje z wyraźną intencją i oczywistymi We/Wy
  • wywóz śmieci jest trudniejszy z pointers I believe
  • łatwiej dyskutować o enkapsulacji, odpowiedzialności
  • keep it simple, stupid (tak, wskaźniki mogą być trudne, ponieważ nigdy nie wiesz, dev następnego projektu)
  • testy jednostkowe to jak spacer po różowym ogrodzie (tylko słowackie wyrażenie?), czyli łatwe
  • no NIL if conditions (NIL można przekazać tam, gdzie oczekiwano wskaźnika)

Moja zasada, napisz jak najwięcej metod, takich jak as:

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:

To pytanie zainspirowało mnie do dokładniejszego zbadania tematu i napisania posta na blogu https://medium.com/gophersland/gopher-vs-object-oriented-golang-4fa62b88c701

 24
Author: Lukas Lukac,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-09-12 08:46:00