& i *
Wskaźnik przechowuje adres wartości w pamięci. &x daje wskaźnik na x. *p podąża za wskaźnikiem do wartości.
Gwiazdka ma dwie role. W typie (*int) oznacza „wskaźnik na int”. W wyrażeniu (*p) oznacza „wartość, na którą wskazuje p”. Wypisanie samego p pokazuje adres w stylu 0xc000012345, który zmienia się między uruchomieniami.
Po co są wskaźniki: współdzielenie zamiast kopiowania
Go przekazuje wszystko przez wartość. Funkcja dostaje kopię każdego argumentu, więc nie może zmienić zmiennej kodu wywołującego. Przekazanie wskaźnika kopiuje zamiast tego adres i obie strony sięgają do tej samej wartości.
Go nie ma referencji w sensie C++. „Przekazywanie przez referencję” w Go oznacza przekazanie wskaźnika przez wartość.
Wskaźniki do struktur
Wskaźniki do struktur to najczęstsze wskaźniki w kodzie Go. Dostęp do pola automatycznie dereferencjonuje: p.Name oznacza (*p).Name.
Metody działają według tej samej logiki: odbiorca wskaźnikowy pozwala metodzie zmienić strukturę. Wybór między odbiorcą wartościowym a wskaźnikowym opisuje strona o metodach.
new
new(T) alokuje wyzerowane T i zwraca jego adres. To to samo co zadeklarowanie zmiennej i pobranie jej adresu.
p := new(int) // *int pointing at 0
var x int
q := &x // same thing, two lines
a := new(Account) // same as &Account{}
W praktyce dla struktur częściej używa się &T{...}, bo pozwala ustawić pola w tym samym wyrażeniu. new przydaje się przy wskaźnikach na typy podstawowe. Nie myl go z make, które buduje slice'y, mapy i kanały i zwraca je gotowe do użycia, a nie jako wskaźniki.
Zwrócenie wskaźnika do zmiennej lokalnej jest bezpieczne
W C zwrócenie adresu zmiennej lokalnej to błąd. W Go to zwykły kod.
Analiza ucieczki w kompilatorze decyduje, czy wartość żyje na stosie, czy na stercie. Nigdy tego nie wybierasz i nigdy nie zwalniasz pamięci: garbage collector odzyskuje ją, gdy nic już na nią nie wskazuje. go build -gcflags=-m wypisuje te decyzje, jeśli chcesz je zobaczyć.
Wskaźniki nil
Wartość zerowa każdego typu wskaźnikowego to nil. Dereferencja nil wywołuje panikę.
Wynik:
1
2
true
recovered: runtime error: invalid memory address or nil pointer dereference
Bez recover program wywróciłby się z tym komunikatem i śladem stosu zaczynającym się od panic: runtime error: invalid memory address or nil pointer dereference i [signal SIGSEGV: segmentation violation ...]. Typowe źródła:
- zmienna wskaźnikowa albo pole struktury, które nigdy nie zostało ustawione
m[key]namap[string]*Tdla brakującego klucza, co zwracanil- zignorowanie błędu:
f, _ := os.Open(path)zostawia przy niepowodzeniufrówne nil - metoda z odbiorcą wskaźnikowym wywołana na wskaźniku nil, która potem czyta pole
Samo wywołanie metody na wskaźniku nil jest dozwolone. Panika pojawia się dopiero wtedy, gdy metoda dotknie pól odbiorcy.
Kiedy używać wskaźnika
Użyj wskaźnika, gdy:
- funkcja lub metoda musi zmienić wartość kodu wywołującego
- wartość to duża struktura, która często jest przekazywana
- typu nie wolno kopiować (zawiera
sync.Mutexalbo reprezentuje unikalny zasób, np. połączenie) - potrzebujesz wyrazić „brak wartości” przez
nil, np. opcjonalne pole w strukturze JSON (*intodróżnia „brak” od0)
Nie używaj wskaźnika, gdy:
- wartość jest mała i tylko do odczytu (
time.Time,Point) - typ już zachowuje się jak referencja: slice'y, mapy, kanały, funkcje i interfejsy rzadko potrzebują
*przed sobą.*[]intalbo*map[string]intto prawie zawsze pomyłka
Wskaźniki nie są automatycznie szybsze. Kopiowanie małej struktury jest tanie, a wskaźnik może wymusić alokację na stercie i dołożyć pracy garbage collectorowi.
Wskaźniki i zmienne pętli
Pobieranie adresu zmiennej pętli jest bezpieczne od Go 1.22, bo każda iteracja ma własną zmienną:
To wypisuje 0 1 2. Przed Go 1.22 (które wprowadziło też range 3) ta sama pętla zapisana jako for i := 0; i < 3; i++ wypisywała 3 3 3, bo wszystkie trzy wskaźniki współdzieliły jedną zmienną. Zwróć uwagę, że &v w for _, v := range items wskazuje na kopię elementu, a nie na element w slice'ie. Aby dostać wskaźnik na sam element, użyj &items[i].
Częste błędy
- Dereferencja bez sprawdzenia. Sprawdzaj
if p == nilwszędzie tam, gdzie nil jest możliwy. - Wskaźnik na wartość z range.
&vwskazuje na kopię; użyj&s[i]. - Wskaźniki na slice'y i mapy. Rzadko potrzebne. Zamiast tego zwróć nowy slice.
- Trzymanie
&s[i]przezappend. Jeśliappendrealokuje pamięć, wskaźnik nadal odnosi się do starej tablicy, a późniejsze zapisy trafiają w miejsce, którego nikt nie widzi.
Najczęściej zadawane pytania
Co oznaczają & i * w Go?
&x pobiera adres x i daje wskaźnik typu *T. *p dereferencjonuje wskaźnik: odczytuje lub zapisuje wartość, na którą wskazuje. W typie *T oznacza "wskaźnik na T".
Czym różni się new od make w Go?
new(T) alokuje wyzerowane T i zwraca *T; działa dla każdego typu. make działa tylko dla slice'ów, map i kanałów i zwraca zainicjalizowaną (nie wyzerowaną i niebędącą wskaźnikiem) wartość tego typu: make(map[string]int) jest gotowe do użycia, a new(map[string]int) to wskaźnik na mapę nil.
Czy w Go można zwrócić wskaźnik do zmiennej lokalnej?
Tak. W odróżnieniu od C jest to bezpieczne. Analiza ucieczki w kompilatorze widzi, że zmienna żyje dłużej niż funkcja, i alokuje ją na stercie. func newInt() *int { x := 5; return &x } to poprawny kod Go.
Co powoduje błąd "invalid memory address or nil pointer dereference" w Go?
Odczyt lub zapis przez wskaźnik nil: zmienną *T, do której nigdy nic nie przypisano, odczyt brakującego klucza z map[K]*V albo funkcję, która zwróciła nil razem z niesprawdzonym błędem. Sprawdzaj nil albo inicjalizuj wskaźnik przed użyciem.
Czy Go ma arytmetykę wskaźników?
Nie. Nie można dodawać do wskaźnika ani indeksować przez niego pamięci. Pakiet unsafe pozwala na to w kodzie niskopoziomowym, bez żadnych gwarancji bezpieczeństwa. Zwykły kod Go używa slice'ów dla ciągłych obszarów pamięci.