range это форма for, которая проходит по коллекции. На каждой итерации она выдаёт одно или два значения, и какие именно, зависит от типа, который вы обходите:
| Операнд | Первое значение | Второе значение | Примечания |
|---|---|---|---|
слайс []T, массив [n]T | индекс int | элемент T, копия | |
указатель на массив *[n]T | индекс | элемент | |
| строка | индекс байта int | rune | декодирует UTF-8 |
мапа map[K]V | ключ K | значение V | случайный порядок |
канал chan T | элемент T | нет | пока канал не закрыт |
целое число n | от 0 до n минус 1 | нет | Go 1.22 |
func(yield func() bool) | нет | нет | Go 1.23 |
func(yield func(V) bool) | V | нет | Go 1.23 |
func(yield func(K, V) bool) | K | V | Go 1.23 |
Какие переменные получать
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
Объявить переменную и не использовать её это ошибка компиляции, поэтому ненужные опускайте или заменяйте на _. for range 3 {} самый короткий способ повторить что-то три раза.
Значение это копия
На каждой итерации элемент копируется в переменную значения. Изменение переменной не меняет коллекцию:
Копирование ещё и стоит времени, когда элементы это большие структуры. Для слайса больших структур for i := range s с обращением s[i] позволяет обойтись без копии.
Строки выдают руны
Обход строки через range декодирует UTF-8. Первое значение это смещение в байтах, с которого начинается каждый символ, поэтому числа могут идти с пропусками:
Чтобы пройти по байтам, а не по символам, используйте цикл по индексу, for i := 0; i < len(s); i++, или range по []byte(s). Подробности про UTF-8 на странице о рунах и байтах.
Мапы: случайный порядок
Порядок обхода мапы не определён, и среда выполнения его рандомизирует, чтобы код не начал зависеть от какого-то одного порядка. Для детерминированного вывода сортируйте ключи:
Удалять записи во время обхода безопасно, и удалённая запись, до которой обход ещё не дошёл, выдана не будет. Добавлять записи можно, но результат непредсказуем: новый ключ может появиться позже в том же цикле, а может и нет. fmt.Println печатает мапу с отсортированными ключами, поэтому последняя строка стабильна.
Каналы: пока не закрыт
range по каналу получает значения, пока канал не будет закрыт и опустошён. Если его никто не закроет, цикл заблокируется навсегда (а если заблокированы все горутины, среда выполнения остановится с fatal error: all goroutines are asleep - deadlock!):
Канал закрывает отправитель, а не получатель. Буферизованные каналы и select разобраны на странице о каналах.
Целые числа (Go 1.22)
range n считает от 0 до n, не включая n. Тип переменной цикла совпадает с типом 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
При отрицательном n цикл тоже выполнится ноль раз. n вычисляется один раз, до начала цикла.
Что и когда вычисляет range
Выражение после range вычисляется один раз, перед первой итерацией. Для слайса range затем использует этот заголовок слайса, поэтому длина фиксирована:
Добавленное 99 не обходится, потому что длина цикла зафиксирована на 3. А вот nums[2] = 30 видно, потому что в этот момент nums всё ещё разделяет базовый массив со слайсом, который обходится. Поменяйте две строки местами, и 30 тоже пропадёт: append превышает ёмкость 3, выделяет новый массив, и запись попадает туда. Обход массива по значению сначала копирует весь массив, поэтому изменение arr[2] не видно; чтобы избежать копии, обходите &arr или arr[:].
range по функциям (Go 1.23)
Начиная с Go 1.23 range принимает и функции-итераторы. Итератор получает колбэк yield, вызывает его по разу на каждое значение и останавливается, когда yield возвращает false (это происходит, когда тело цикла выполняет break или return):
iter.Seq[V] и iter.Seq2[K, V] это стандартные имена двух форм итератора. Стандартная библиотека возвращает их из slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All, а в Go 1.24 из strings.SplitSeq, strings.Lines и bytes.SplitSeq. Итератор обязан учитывать результат yield: повторный вызов yield после того, как он вернул false, вызывает панику.
Переменные цикла своя на каждой итерации (Go 1.22)
Начиная с Go 1.22 переменные, объявленные в for ... range, создаются заново на каждой итерации. Захват v в замыкании или горутине захватывает значение этой итерации, а не общую переменную, в которой в итоге окажется последний элемент. До Go 1.22 внутри цикла нужна была строка v := v; теперь она лишняя. Правило определяется версией go в go.mod, поэтому старый модуль сохраняет старое поведение.
Частые ошибки
- Взятие адреса переменной значения. До Go 1.22
ptrs = append(ptrs, &v)каждый раз сохранял один и тот же адрес. Начиная с 1.22 адреса разные, но каждый указывает на копию, а не на элемент слайса. Чтобы указывать внутрь слайса, используйте&items[i]. - Расчёт на порядок в мапе. Вывод, который выглядит отсортированным в маленьком тесте, отсортированным не останется.
- range по каналу, который никто не закрывает. Цикл и горутина, в которой он выполняется, ждут вечно.
- range по большому массиву по значению. Сначала копируется весь массив. Обходите его слайс.
Часто задаваемые вопросы
Что возвращает range в Go?
Зависит от операнда. Для слайса или массива: индекс и элемент. Для строки: индекс байта и руну. Для мапы: ключ и значение. Для канала: каждое полученное значение. Для целого числа n (Go 1.22): от 0 до n минус 1. Для функции-итератора (Go 1.23): то, что отдаёт функция. Вторую переменную можно опустить, а первую отбросить через _.
Почему изменение значения в range не меняет мой слайс?
Переменная значения это копия элемента. for _, v := range items { v.Price = 0 } меняет только копию. Используйте индекс: for i := range items { items[i].Price = 0 } или обходите слайс указателей.
Можно ли делать range по целому числу в Go?
Да, начиная с Go 1.22: for i := range 5 { ... } выполняется с i от 0 до 4. for range 5 { ... } повторяет тело пять раз без переменной. В go.mod модуля должно быть указано go 1.22 или новее.
Безопасно ли удалять из map во время range?
Да. Удалённая запись, до которой обход ещё не дошёл, просто не будет выдана, а удалять текущую запись можно. Добавлять записи во время цикла тоже разрешено, но новая запись может попасть в обход, а может и нет, так что на это не полагайтесь.