Menu

range в Golang: все формы, что возвращает и подводные камни

Что возвращает for range для слайсов, массивов, строк, мап, каналов, целых чисел и функций-итераторов, почему переменная значения это копия, что происходит при изменении слайса или мапы во время обхода и range по функциям в Go 1.23.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

range это форма for, которая проходит по коллекции. На каждой итерации она выдаёт одно или два значения, и какие именно, зависит от типа, который вы обходите:

ОперандПервое значениеВторое значениеПримечания
слайс []T, массив [n]Tиндекс intэлемент T, копия
указатель на массив *[n]Tиндексэлемент
строкаиндекс байта intruneдекодирует 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)KVGo 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?

Да. Удалённая запись, до которой обход ещё не дошёл, просто не будет выдана, а удалять текущую запись можно. Добавлять записи во время цикла тоже разрешено, но новая запись может попасть в обход, а может и нет, так что на это не полагайтесь.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ