Menu

Range w Golang: wszystkie formy, co zwraca i gdzie są pułapki

Co zwraca for range dla slice'ów, tablic, stringów, map, kanałów, liczb całkowitych i funkcji iteratorów, dlaczego zmienna wartości jest kopią, co się dzieje przy modyfikowaniu slice'a lub mapy w trakcie range i jak działa range po funkcji w Go 1.23.

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

range to forma for, która przechodzi po kolekcji. Daje jedną lub dwie wartości na iterację, a to, czym są te wartości, zależy od typu, po którym iterujesz:

OperandPierwsza wartośćDruga wartośćUwagi
slice []T, tablica [n]Tindeks intelement T, kopia
wskaźnik na tablicę *[n]Tindekselement
stringindeks bajtu intrunedekoduje UTF-8
mapa map[K]Vklucz Kwartość Vlosowa kolejność
kanał chan Telement Tbrakdo zamknięcia kanału
liczba całkowita nod 0 do n minus 1brakGo 1.22
func(yield func() bool)brakbrakGo 1.23
func(yield func(V) bool)VbrakGo 1.23
func(yield func(K, V) bool)KVGo 1.23

Wybór zmiennych, które dostajesz

for i, v := range s {}  // both
for i := range s {}     // index (or key) only
for _, v := range s {}  // value only
for range s {}          // neither: just repeat len(s) times

Zadeklarowanie nieużywanej zmiennej to błąd kompilacji, więc pomiń albo zastąp przez _ te, których nie potrzebujesz. for range 3 {} to najkrótszy sposób, żeby coś powtórzyć trzy razy.

Wartość jest kopią

W każdej iteracji element jest kopiowany do zmiennej wartości. Zmiana zmiennej nie zmienia kolekcji:

Kopiowanie kosztuje też czas, gdy elementy są dużymi strukturami. Dla slice'a dużych struktur for i := range s z s[i] pozwala uniknąć kopii.

Stringi zwracają runy

Range po stringu dekoduje UTF-8. Pierwsza wartość to przesunięcie w bajtach, od którego zaczyna się każdy znak, więc może przeskakiwać liczby:

Aby przejść po bajtach zamiast po znakach, użyj pętli z indeksem, for i := 0; i < len(s); i++, albo range po []byte(s). Szczegóły UTF-8 opisuje strona o runach i bajtach.

Mapy: losowa kolejność

Kolejność iteracji po mapie nie jest określona, a runtime ją losuje, żeby kod nie mógł zacząć zależeć od jednej kolejności. Dla deterministycznego wyniku posortuj klucze:

Usuwanie wpisów w trakcie range jest bezpieczne, a usunięty wpis, do którego pętla jeszcze nie dotarła, nie zostanie zwrócony. Dodawanie wpisów jest dozwolone, ale nieprzewidywalne: nowy klucz może pojawić się później w tej samej pętli albo nie. fmt.Println wypisuje mapę z posortowanymi kluczami, dlatego ostatnia linia jest stabilna.

Kanały: do zamknięcia

Range po kanale odbiera wartości, dopóki kanał nie zostanie zamknięty i opróżniony. Jeśli nikt go nie zamknie, pętla blokuje się na zawsze (a jeśli wszystkie gorutyny są zablokowane, runtime zatrzymuje się z fatal error: all goroutines are asleep - deadlock!):

Kanał zamyka nadawca, nigdy odbiorca. Kanały buforowane i select opisuje strona o kanałach.

Liczby całkowite (Go 1.22)

range n liczy od 0 do n, ale bez n. Typ zmiennej pętli to typ n:

for i := range 3 {}         // i is int: 0, 1, 2
for i := range uint8(3) {}  // i is uint8
for range 0 {}              // runs zero times

Ujemne n również wykonuje się zero razy. n jest obliczane raz, przed startem pętli.

Co range oblicza i kiedy

Wyrażenie po range jest obliczane raz, przed pierwszą iteracją. Dla slice'a range używa potem tego nagłówka slice'a, więc jego długość jest stała:

Dołączone 99 nigdy nie zostaje odwiedzone, bo długość pętli ustalono na 3. Ale nums[2] = 30 jest widoczne, bo w tym momencie nums nadal współdzieli tablicę bazową ze slice'em, po którym iteruje pętla. Zamień te dwie linie miejscami, a 30 też zniknie: append przekracza pojemność 3, alokuje nową tablicę i zapis trafia właśnie tam. Range po wartości tablicy najpierw kopiuje całą tablicę, więc zmiana arr[2] nie jest widoczna; iteruj po &arr albo arr[:], żeby uniknąć kopii.

Range po funkcjach (Go 1.23)

Od Go 1.23 range przyjmuje też funkcje iteratorów. Iterator przyjmuje callback yield, wywołuje go raz na każdą wartość i kończy, gdy yield zwróci false (co dzieje się, gdy ciało pętli wykona break albo return):

iter.Seq[V] i iter.Seq2[K, V] to standardowe nazwy dwóch kształtów iteratorów. Biblioteka standardowa zwraca je z slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All, a w Go 1.24 także z strings.SplitSeq, strings.Lines i bytes.SplitSeq. Iterator musi respektować wartość zwracaną przez yield: ponowne wywołanie yield po tym, jak zwróciło false, wywołuje panikę.

Zmienne pętli są osobne dla każdej iteracji (Go 1.22)

Od Go 1.22 zmienne deklarowane przez for ... range są nowe w każdej iteracji. Przechwycenie v w domknięciu lub gorutynie przechwytuje wartość z tej iteracji, a nie wspólną zmienną, która na końcu trzyma ostatni element. Przed Go 1.22 potrzebne było v := v wewnątrz pętli; teraz ta linia jest zbędna. Regułę ustala wersja go w go.mod, więc stary moduł zachowuje stare zachowanie.

Częste błędy

  • Pobieranie adresu zmiennej wartości. Przed Go 1.22 ptrs = append(ptrs, &v) za każdym razem zapisywało ten sam adres. Od 1.22 zapisuje różne adresy, ale każdy wskazuje na kopię, a nie na element slice'a. Użyj &items[i], żeby wskazywać do wnętrza slice'a.
  • Oczekiwanie kolejności w mapie. Wynik, który w małym teście wygląda na posortowany, nie pozostanie posortowany.
  • Range po kanale, którego nikt nie zamyka. Pętla i gorutyna, która ją wykonuje, czekają w nieskończoność.
  • Range po dużej tablicy przez wartość. Najpierw kopiowana jest cała tablica. Zamiast tego iteruj po jej slice'ie.

Najczęściej zadawane pytania

Co zwraca range w Go?

To zależy od operandu. Dla slice'a lub tablicy: indeks i element. Dla stringa: indeks bajtu i runę. Dla mapy: klucz i wartość. Dla kanału: każdą odebraną wartość. Dla liczby całkowitej n (Go 1.22): od 0 do n minus 1. Dla funkcji iteratora (Go 1.23): to, co funkcja przekaże przez yield. Możesz pominąć drugą zmienną albo odrzucić pierwszą przez _.

Dlaczego zmiana wartości z range nie zmienia mojego slice'a?

Zmienna wartości to kopia elementu. for _, v := range items { v.Price = 0 } zmienia tylko kopię. Użyj zamiast tego indeksu: for i := range items { items[i].Price = 0 } albo iteruj po slice'ie wskaźników.

Czy w Go można użyć range na liczbie całkowitej?

Tak, od Go 1.22: for i := range 5 { ... } wykonuje się z i od 0 do 4. for range 5 { ... } powtarza pięć razy bez zmiennej. go.mod modułu musi zawierać go 1.22 lub nowsze.

Czy można bezpiecznie usuwać z mapy w trakcie range po niej?

Tak. Usunięcie wpisu, do którego pętla jeszcze nie dotarła, oznacza, że nie zostanie zwrócony, a usunięcie bieżącego wpisu jest w porządku. Dodawanie wpisów w trakcie pętli też jest dozwolone, ale nowy wpis może zostać odwiedzony albo nie, więc nie polegaj na tym.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ