Menu

Wskaźniki w Golang: & i *, new, nil i kiedy ich używać

Wskaźnik przechowuje adres wartości. Poznaj & i *, new, wskaźniki do struktur, dowiedz się, dlaczego można bezpiecznie zwrócić wskaźnik do zmiennej lokalnej, kiedy używać wskaźników i skąd bierze się panika nil pointer dereference.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

& 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] na map[string]*T dla brakującego klucza, co zwraca nil
  • zignorowanie błędu: f, _ := os.Open(path) zostawia przy niepowodzeniu f ró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.Mutex albo reprezentuje unikalny zasób, np. połączenie)
  • potrzebujesz wyrazić „brak wartości” przez nil, np. opcjonalne pole w strukturze JSON (*int odróżnia „brak” od 0)

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ą. *[]int albo *map[string]int to 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 == nil wszędzie tam, gdzie nil jest możliwy.
  • Wskaźnik na wartość z range. &v wskazuje na kopię; użyj &s[i].
  • Wskaźniki na slice'y i mapy. Rzadko potrzebne. Zamiast tego zwróć nowy slice.
  • Trzymanie &s[i] przez append. Jeśli append realokuje 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ