W Go istnieją różne sposoby zwracania struct
wartości lub jej części. Dla indywidualnych widziałem:
type MyStruct struct {
Val int
}
func myfunc() MyStruct {
return MyStruct{Val: 1}
}
func myfunc() *MyStruct {
return &MyStruct{}
}
func myfunc(s *MyStruct) {
s.Val = 1
}
Rozumiem różnice między nimi. Pierwszy zwraca kopię struktury, drugi wskaźnik do wartości struktury utworzonej w funkcji, trzeci oczekuje, że istniejąca struktura zostanie przekazana i zastępuje wartość.
Widziałem, że wszystkie te wzorce są używane w różnych kontekstach, zastanawiam się, jakie są najlepsze praktyki w odniesieniu do nich. Kiedy użyjesz które? Na przykład pierwszy może być odpowiedni dla małych struktur (ponieważ narzut jest minimalny), drugi dla większych. I trzeci, jeśli chcesz być wyjątkowo wydajnym pod względem pamięci, ponieważ możesz łatwo ponownie użyć jednej instancji struktury między rozmowami. Czy są jakieś najlepsze praktyki dotyczące tego, kiedy stosować?
Podobnie to samo pytanie dotyczące plasterków:
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} }
}
Ponownie: jakie są tutaj najlepsze praktyki. Wiem, że plasterki są zawsze wskaźnikami, więc zwrócenie wskaźnika do plasterka nie jest przydatne. Jednak czy powinienem zwrócić kawałek wartości struct, kawałek wskaźników do struktur, czy powinienem przekazać wskaźnik do plasterka jako argument (wzorzec używany w API App Engine )?
new(MyStruct)
:) Ale tak naprawdę nie ma różnicy między różnymi metodami przydzielania wskaźników i zwracania ich.Odpowiedzi:
tl; dr :
Jeden przypadek, w którym powinieneś często używać wskaźnika:
Niektóre sytuacje, w których nie potrzebujesz wskaźników:
Wytyczne przeglądu kodu sugerują przekazywanie małych struktur, takich jak
type Point struct { latitude, longitude float64 }
, a może nawet nieco większych, wartości, chyba że wywoływana funkcja musi mieć możliwość ich modyfikacji.bytes.Replace
wymaga 10 słów argumentów (trzy plasterki i anint
).W przypadku plasterków nie trzeba przekazywać wskaźnika, aby zmieniać elementy tablicy.
io.Reader.Read(p []byte)
zmienia na przykład bajtyp
. Prawdopodobnie jest to szczególny przypadek „traktowania małych struktur jak wartości”, ponieważ wewnętrznie przekazujesz małą strukturę zwaną nagłówkiem wycinka (zobacz wyjaśnienie Russa Coxa (rsc) ). Podobnie nie potrzebujesz wskaźnika, aby modyfikować mapę lub komunikować się na kanale .W przypadku plasterków nastąpi ponowne odbarwienie (zmiana początku / długości / pojemności), wbudowane funkcje, takie jak
append
akceptowanie wartości plasterka i zwracanie nowej. Naśladowałbym to; unika aliasingu, zwracanie nowego wycinka pomaga zwrócić uwagę na fakt, że nowa tablica może być przydzielona i jest znana dzwoniącym.interface{}
parametru.Mapy, kanały, ciągi oraz wartości funkcji i interfejsów , takie jak wycinki, są wewnętrznymi odniesieniami lub strukturami, które już zawierają odwołania, więc jeśli próbujesz tylko uniknąć kopiowania podstawowych danych, nie musisz przekazywać do nich wskaźników . (rsc napisał osobny post na temat przechowywania wartości interfejsów ).
flag.StringVar
bierze to*string
z tego powodu.Gdzie używasz wskaźników:
Zastanów się, czy twoja funkcja powinna być metodą na dowolnej strukturze, do której potrzebujesz wskaźnika. Ludzie oczekują wielu metod
x
modyfikacjix
, więc zmodyfikowanie struktury odbiornika może pomóc zminimalizować niespodziankę. Istnieją wytyczne dotyczące tego, kiedy odbiorniki powinny być wskaźnikami.Funkcje, które mają wpływ na ich nie-odbiorcze parametry, powinny to wyjaśnić w godoc, lub jeszcze lepiej, godoc i nazwa (jak
reader.WriteTo(writer)
).Wspominasz o zaakceptowaniu wskaźnika, aby uniknąć przydziału, umożliwiając ponowne użycie; zmiana interfejsów API w celu ponownego wykorzystania pamięci to optymalizacja, którą opóźnię, dopóki nie okaże się, że alokacje wiążą się z nietrywialnymi kosztami, a następnie poszukałbym sposobu, który nie wymuszałby bardziej skomplikowanego interfejsu API dla wszystkich użytkowników:
bytes.Buffer
.Reset()
metodę przywrócenia obiektu do stanu pustego, tak jak oferują niektóre typy stdlib. Użytkownicy, którym nie zależy lub nie mogą zapisać przydziału, nie muszą do niego dzwonić.existingUser.LoadFromJSON(json []byte) error
może być zawiniętyNewUserFromJSON(json []byte) (*User, error)
. Ponownie przesuwa wybór między lenistwem a szczypaniem przydziałów dla poszczególnych rozmówców.sync.Pool
pewne szczegóły. Jeśli dana alokacja powoduje duże obciążenie pamięci, masz pewność, że wiesz, kiedy alokacja nie jest już używana i nie masz lepszej optymalizacji,sync.Pool
może pomóc. (CloudFlare opublikował przydatny (wstępnysync.Pool
) post na blogu o recyklingu.)Na koniec, czy wycinki powinny zawierać wskaźniki: wycinki wartości mogą być przydatne, oszczędzając przydziały i pominięcia pamięci podręcznej. Mogą istnieć blokery:
NewFoo() *Foo
a nie pozwolić, aby Go zainicjował wartość zerową .append
kopiuje elementy, gdy rośnie podstawowa tablica . Wskaźniki, które dostałeś przedappend
wskazaniem niewłaściwego miejsca po, kopiowanie może być wolniejsze w przypadku dużych struktur, a na przykładsync.Mutex
kopiowanie nie jest dozwolone. Wstaw / usuń na środku i sortuj podobnie przesuwaj elementy.Ogólnie rzecz biorąc, wycinki wartości mogą mieć sens, jeśli albo dostaniesz wszystkie swoje przedmioty z góry i nie przeniesiesz ich (np. Nie będzie więcej
append
po pierwszej konfiguracji), lub jeśli będziesz je ciągle przenosić, ale jesteś pewien, że to OK (brak / ostrożne użycie wskaźników do elementów, elementy są wystarczająco małe, aby skutecznie kopiować itp.). Czasami musisz przemyśleć lub zmierzyć specyfikę swojej sytuacji, ale jest to przybliżony przewodnik.źródło
Replace(s, old, new []byte, n int) []byte
; s, stary i nowy to po trzy słowa ( nagłówki wycinka(ptr, len, cap)
) in int
jedno słowo, więc 10 słów, które przy ośmiu bajtach / słowo to 80 bajtów.Trzy główne powody, dla których chcesz używać odbiorników metod jako wskaźników:
„Po pierwsze i najważniejsze, czy metoda wymaga modyfikacji odbiornika? Jeśli tak, odbiornik musi być wskaźnikiem”.
„Drugą kwestią jest rozważenie wydajności. Jeśli odbiornik jest duży, na przykład duża konstrukcja, korzystanie ze wskaźnika odbiornika będzie znacznie tańsze”.
„Następna jest spójność. Jeśli niektóre metody tego typu muszą mieć odbiorniki wskaźnikowe, pozostałe też powinny, więc zestaw metod jest spójny bez względu na sposób użycia typu”
Odniesienie: https://golang.org/doc/faq#methods_on_values_or_pointers
Edycja: Inną ważną rzeczą jest poznanie rzeczywistego „typu”, który wysyłasz do funkcji. Typ może być „typem wartości” lub „typem odniesienia”.
Nawet gdy wycinki i mapy działają jako odniesienia, możemy chcieć przekazać je jako wskaźniki w scenariuszach, takich jak zmiana długości wycinka w funkcji.
źródło
Przypadek, w którym zwykle musisz zwrócić wskaźnik, dotyczy budowy instancji jakiegoś zasobowego lub współdzielonego zasobu . Często odbywa się to za pomocą funkcji z prefiksem
New
.Ponieważ reprezentują określoną instancję czegoś i mogą potrzebować skoordynować pewne działanie, nie ma sensu generowanie zduplikowanych / kopiowanych struktur reprezentujących ten sam zasób - więc zwrócony wskaźnik działa jak uchwyt do samego zasobu .
Kilka przykładów:
func NewTLSServer(handler http.Handler) *Server
- utworzyć instancję serwera WWW w celu przetestowaniafunc Open(name string) (*File, error)
- zwróć uchwyt dostępu do plikuW innych przypadkach zwracane są wskaźniki tylko dlatego, że struktura może być zbyt duża, aby domyślnie ją skopiować:
func NewRGBA(r Rectangle) *RGBA
- przydzielić obraz do pamięciAlternatywnie, można uniknąć bezpośredniego zwracania wskaźników, zamiast tego zwracając kopię struktury zawierającą wskaźnik wewnętrznie, ale być może nie jest to uważane za idiomatyczne:
źródło
Jeśli możesz (np. Nieudostępniony zasób, który nie musi być przekazywany jako odniesienie), użyj wartości. Z następujących powodów:
Powód 1 : przydzielisz mniej przedmiotów na stosie. Przydzielanie / zwalnianie ze stosu jest natychmiastowe, ale przydzielanie / zwalnianie na stercie może być bardzo kosztowne (czas przydzielania + odśmiecanie). Możesz zobaczyć kilka podstawowych liczb tutaj: http://www.macias.info/entry/201802102230_go_values_vs_references.md
Powód 2 : zwłaszcza jeśli przechowujesz zwrócone wartości w plasterkach, twoje obiekty pamięci będą bardziej zwarte w pamięci: zapętlenie plasterka, w którym wszystkie elementy są ciągłe, jest znacznie szybsze niż iteracja plasterka, w którym wszystkie elementy są wskaźnikami do innych części pamięci . Nie dla kroku pośredniego, ale dla zwiększenia braków pamięci podręcznej.
Łamacz mitów : typowa linia pamięci podręcznej x86 ma 64 bajty. Większość struktur jest mniejsza. Czas kopiowania linii pamięci podręcznej jest podobny do kopiowania wskaźnika.
Tylko jeśli krytyczna część twojego kodu jest powolna, spróbowałbym mikrooptymalizacji i sprawdziłbym, czy użycie wskaźników poprawia nieco szybkość, kosztem mniejszej czytelności i łatwości zarządzania.
źródło