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:
| Operand | Pierwsza wartość | Druga wartość | Uwagi |
|---|---|---|---|
slice []T, tablica [n]T | indeks int | element T, kopia | |
wskaźnik na tablicę *[n]T | indeks | element | |
| string | indeks bajtu int | rune | dekoduje UTF-8 |
mapa map[K]V | klucz K | wartość V | losowa kolejność |
kanał chan T | element T | brak | do zamknięcia kanału |
liczba całkowita n | od 0 do n minus 1 | brak | Go 1.22 |
func(yield func() bool) | brak | brak | Go 1.23 |
func(yield func(V) bool) | V | brak | Go 1.23 |
func(yield func(K, V) bool) | K | V | Go 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.