Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 15 из 25 ~20 мин +50 XP

Range: перебор коллекций

Range go берёт на себя самый скучный вид цикла — перебор коллекций: for i, v := range items сам отдаёт индекс и значение по очереди, без счётчика, условия и шага в заголовке. В уроке про [цикл for](/courses/go-basics/lessons/13-cikl-for/) мы собирали такой перебор вручную, в уроке про [break и continue](/courses/go-basics/lessons/14-while-break-continue/) — крутили индекс в «пока»-форме; здесь та же работа делается одной строкой. Разберём range по срезу и массиву, range по строке (байтовые смещения и руны), итерацию по карте (порядок не гарантирован — объясним, что с этим делать) и коротко range по каналу. Плюс два вопроса, которые решают всё: индекс или значение брать в цикл, и куда девать ненужную переменную.

Содержание урока · 11 разделов

Range go: перебор без ручного счётчика#

Заказов в срезе двадцать, и каждый нужно записать в лог. По образцу уроков 13 и 14 вы бы написали: счётчик i := 0, условие i < len(orders), шаг i++, а внутри — orders[i]. Три места в заголовке, и в каждом можно ошибиться: начать с единицы, написать <= вместо <, забыть шаг. Range go убирает весь этот ритуал: заголовок for i, v := range orders читается как «для каждой пары индекс-значение из orders», и машина сама гоняет счётчик от нуля до конца. Никакого len(), никакой арифметики, нулевой шанс выйти за границу — range останавливается ровно на последнем элементе.

Важно понимать: range — не новый цикл. Цикл в языке по-прежнему один, и range — это форма его заголовка, как «пока»-форма и вечный for из прошлого урока. Тело такое же: внутри работают break и continue с теми же правилами, что мы разбирали в уроке про break и continue. Переменные i и v живут только внутри цикла — после него их не существует, это та же дисциплина областей видимости, что у счётчика классического for. Запустим первый перебор.

Песочница: первый range по срезу

Запустите: три пары «индекс — значение» и финальная строка после цикла. Замените срез на цены prices := []int{45, 89, 520} и напечатайте только v — индекс стал не нужен, и об этом ниже. Добавьте в тело if v == "груша" { break } — перебор остановится на груше, до сливы дело не дойдёт. Напоследок попробуйте написать литерал прямо в заголовке: for i, v := range []string{"яблоко"} — в основном движке песочницы запись сработает, но объявлять коллекцию в переменную всё равно читабельнее. А в резервном упрощённом режиме литерал в range-заголовке не парсится — ответит «ожидалось присваивание или вызов», так что привычка объявлять коллекцию в переменную выручает в любом движке.

Вывод

Запустите код, чтобы увидеть вывод.

Range по срезу go: индекс, значение и подчёркивание#

Перебор среза go чаще всего выглядит как в примере выше: две переменные в заголовке, первая — индекс, вторая — копия значения. Дальше начинается гибкость. Оставьте одну переменную — for i := range prices — и в неё попадёт индекс: это форма для изменения среза на месте, prices[i] *= 2, и для случаев, когда нужен номер позиции. Значение не нужно вовсе? Поставьте на его место подчёркивание: for _, v := range prices читается как «мне важны только значения». Подчёркивание — пустая переменная-приёмник: присваивать в неё можно, читать нельзя, и компилятор не держит за лишние имена.

Теперь главное, о чём спотыкаются: v — это копия. Присвоите ей v = v 10 — изменится копия, срез останется прежним; копия умирает на следующем обороте. Чтобы поменять сам срез, пишите по индексу — prices[i] = prices[i] 2. Аккумулятор для суммы, как и в уроке про классический for, объявляйте до цикла: индекс и значение из заголовка range умирают вместе с циклом. Эти три формы — две переменные, одна, с подчёркиванием — покрывают девять из десяти задач перебора; проверим знание форм вопросом.

Проверьте себя: что в единственной переменной

prices := []int{10, 20, 30}, и цикл записан с одной переменной: for v := range prices { fmt.Println(v) }. Что напечатает программа?

Range по строке: байтовые смещения и руны#

Range по строке — самый хитрый из всех: здесь знание про UTF-8 из предыдущих уроков превращается в практику. В настоящем Go заголовок for i, r := range word отдаёт на каждом обороте две вещи: i — байтовое смещение, где начинается очередной символ, а r — сам символ, его числовой код руны. Для ASCII-строки всё скучно: латинская буква занимает один байт, поэтому смещения идут подряд 0, 1, 2 и коды совпадают с привычной таблицей — 'h' это 104. А вот кириллица в UTF-8 занимает два байта на букву, и для слова «го» настоящий Go даст пары 0 и 1075, 2 и 1086: смещение перепрыгивает через два, а в r попадает целая буква. Именно поэтому range по строке считает не буквы, а руны — и len() той же строки вернёт байты, как мы уже видели в уроке 14 на примере len("го") = 4.

Основной движок песочницы исполняет настоящий Go, поэтому всё сказанное проверяется прямо здесь — и латиница, и кириллица. Оговорка одна: если WASM-движок по какой-то причине недоступен, песочница автоматически включает резервный упрощённый интерпретатор — тот идёт по строке по байтам: каждая итерация один байт, в переменной код от 0 до 255, у кириллицы вдвое больше оборотов. Для ASCII разницы с настоящим Go нет ни в смещениях, ни в кодах. Ниже — рун-пример, потом проверка в песочнице.

Range по строке в настоящем Go

GO
package main

import "fmt"

func main() {
        // i — байтовое смещение руны, r — код символа
        for i, r := range "го" {
                fmt.Printf("смещение %d: %c (код %d)\n", i, r, r)
        }
}

В настоящем Go — и в основном движке песочницы — программа напечатает «смещение 0: г (код 1075)» и «смещение 2: о (код 1086)»: глагол %c рисует символ по коду, %d — число. Кнопки запуска у блока нет: резервный упрощённый движок идёт по байтам и дал бы четыре оборота — 0 и 208, 1 и 179, 2 и 208, 3 и 190 (байты «г» и «о» в UTF-8), а блок обязан вести себя одинаково в любом режиме. Руны проверьте сами: вставьте этот код в песочницу и запустите на основном движке.

Песочница: range по строке

Запустите: шесть оборотов, смещения подряд, коды 97–111 — для ASCII это ровно то, что делает настоящий Go: буква занимает один байт, коды совпадают с таблицей, оборотов ровно len(). Теперь замените строку на "голанг" и запустите снова: оборотов останется шесть — по одному на руну, смещения пойдут 0, 2, 4, 6, 8, 10, а в r придут коды букв 1075, 1086 и дальше. Если вдруг увидите двенадцать оборотов с кодами 0–255 — вы попали в резервный упрощённый движок, он идёт по байтам; на основном движке кириллица всегда рунная.

Вывод

Запустите код, чтобы увидеть вывод.

Что выведет код?

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        for i, b := range "hi" {
                fmt.Println(i, b)
        }
}

Итерация по карте: порядок никто не обещал#

Итерация по карте выглядит так же, как по срезу: for name, qty := range stock отдаёт ключ и значение. Можно взять и только ключи: for name := range stock — частая заготовка для проверки наличия или сбора списка имён. Но у карт есть свойство, которое меняет стиль кода: порядок обхода не гарантирован. В настоящем Go он вообще разный при каждом запуске — язык намеренно перемешивает порядок, чтобы никто не построил на нём логику. Это не баг, а дизайнерское решение: раз порядок не обещан, программы пишут так, чтобы он был не важен.

Основной движок песочницы исполняет настоящий Go, поэтому и здесь порядок при каждом запуске свой: два запуска одной программы могут напечатать ключи в разном порядке — не полагайтесь на него никогда. Код, который работает только потому, что «яблоко» шло после «груши», сломается при первом же перезапуске. Нужен конкретный порядок — его делают явно: ключи собирают в срез, сортируют пакетом sort (sort.Strings работает в песочнице — проверено пробником) и уже по срезу ключей идёт range. Ровно так устроен ранимый пример ниже: sort.Strings делает его вывод одинаковым при каждом запуске и в любом движке.

Песочница: сумма по карте

Запустите: три товара напечатаны по алфавиту, итог 654 — и при повторном запуске порядок не изменится. Ключи сначала собираются в срез и сортируются sort.Strings, поэтому вывод одинаков в любом движке; уберите sort.Strings — и в основном движке порядок снова станет случайным при каждом запуске. Сумма от порядка не зависит: сложение коммутативно, и это правильный стиль — считать так, чтобы порядок был не важен. Добавьте четвёртый товар "кефир": 60 — он встанет на своё место в алфавитном списке. Кстати, обратите внимание: литерал карты объявлен в переменную — в заголовке range стоит только имя.

Вывод

Запустите код, чтобы увидеть вывод.

Проверьте себя: порядок карты

Одну и ту же программу с range по карте map[string]int запустили на настоящем Go дважды подряд. Что с порядком обхода ключей?

Range по каналу — коротко и честно#

Каналы — тема отдельного курса: горутины, буферы, select. Здесь только заготовка, чтобы вы узнали конструкцию в чужом коде. Range по каналу читает значения, пока они поступают: for v := range ch — каждый оборот забирает очередное значение, а заканчивается цикл, когда канал закрыт функцией close(ch): она говорит «данных больше не будет». Так воркеры получают задачи, пока отправитель не объявит конец очереди.

В основном движке песочницы этот цикл прогнать можно: close(ch) там есть, и range по закрытому каналу напечатает задачи и спокойно выйдет из цикла. Опасность другая — range по незакрытому каналу блокируется навсегда, как только данные кончились: основной движок оборвёт такое ожидание лимитом времени («превышен лимит времени»), а в резервном упрощённом движке close вовсе нет — на close(ch) он отвечает «неопределённая переменная close», и range по открытому каналу зависает без всякого сообщения. Поэтому живые канал-примеры — в курсе go-advanced, где горутины и каналы разобраны по-настоящему. Запомните пока одно: увидели for v := range ch — читайте как «получай задачи, пока канал не закрыли».

Индекс или значение: когда что выбирать#

Три формы заголовка — это три ответа на вопрос «что мне нужно от элемента». Нужен сам элемент, чтобы его прочитать? for _, v — подчёркивание сразу говорит читателю: позиция не важна. В суммировании и поиске максимума я пишу именно так — лишний индекс в заголовке отвлекает. Нужно менять элементы или знать позицию? for i := range с записью по индексу: prices[i] *= 2 меняет срез, а не копию. Нужно и то и другое — печать «товар 3 из 10» рядом со значением, проверка соседа по индексу? Берите обе переменные: for i, v := range.

Одна настоящая деталь напоследок: значение из range копируется целиком. Для чисел и строк это ничего не стоит, для крупной структуры в настоящем Go копия — лишняя работа, и там предпочитают for i := range без второй переменной. В песочнице разница не видна, но привычка дешёвая: перебираете тяжёлые структуры — берите индекс. Для обычных же срезов две переменные читаются лучше, и экономия на букве не стоит ясности.

Изменение среза на месте

GO
package main

import "fmt"

func main() {
        prices := []int{100, 200, 300}
        for i := range prices {
                prices[i] *= 2 // меняем сам срез, а не копию
        }
        fmt.Println(prices)
        sum := 0
        for _, p := range prices {
                sum += p
        }
        fmt.Println("сумма:", sum)
}

Этот код запускается — прогоните. Первый цикл оставил в заголовке только индекс и удвоил каждый элемент записью по месту: в выводе [200 400 600]. Второй цикл — образец противоположной задачи: значение нужно только для чтения, индекс спрятан в подчёркивание, аккумулятор объявлен до цикла и печатается после. Две формы рядом — и видно, как выбор переменных в заголовке сам документирует намерение.

Что выведет код: ловушка копии

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        nums := []int{1, 2, 3}
        for _, v := range nums {
                v = v * 10
        }
        fmt.Println(nums)
}

Массивы, структуры и range без переменных#

Range работает не только со срезами. Массив перебирается тем же заголовком: arr := [3]int{5, 0, 9} и for i, v := range arr — индексы от нуля до длины, нулевые элементы на месте. Одна оговорка на будущее: в настоящем Go запись var arr [3]int честно даёт массив длины три, но в резервном движке песочницы она ломается (нулевая длина), поэтому мы везде пишем литерал arr := [3]int{...}; подробный разбор массивов — в следующем уроке. Срез структур перебирается так же: for _, it := range cart, и в it попадает копия структуры — поля читаются точкой, менять элемент нужно по индексу.

Есть и четвёртая форма — вообще без переменных: for range words { n++ } — цикл без обращения к элементам, чистый счётчик оборотов. Пригодится, когда важен сам факт перебора: посчитать записи, прогнать что-то по числу элементов. И последнее: в свежих версиях Go (с 1.22) range работает и по числам — for i := range 5 даёт 0, 1, 2, 3, 4; основной движок песочницы такую запись исполняет, а в резервном упрощённом она не поддержана — там выручает классический for из урока 13. Полный план тем курса — на странице курса go-basics.

Вставьте пропуски: сумма цен

Соберите суммирование цен: переберите срез, игнорируя индекс, и накопите итог в аккумуляторе, объявленном до цикла. Правильная программа печатает «итого: 450».

prices := []int{150, 200, 100} total := 0 for _, price := prices { total price } fmt.Println("итого:", total)

Соберите программу: перебор языков

Соберите перебор среза языков: объявление среза в переменную, range с индексом и именем, печать каждой пары и финальная строка после цикла. Правильная программа печатает «0 go», «1 rust», «2 python» и «перебор закончен».

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Шпаргалка: что даёт range для каждой коллекции#

КоллекцияОдна переменнаяДве переменныеПорядок обхода
Срез, массивиндексиндекс и значение (копия)по индексам слева направо
Строкаиндекс байтабайтовое смещение и рунапо рунам слева направо; в резервном движке — по байтам
Картаключключ и значениеслучайный при каждом запуске; в резервном движке — отсортированный
Канал—значениепорядок поступления; читает до close

Карточки: range и коллекции

Термин

Как выглядит range по срезу и что дают его переменные?

нажмите, чтобы перевернуть
Определение

for i, v := range slice: i — индекс, v — копия значения. Одна переменная — индекс; подчёркивание пропускает ненужное. Индекс и значение видны только внутри цикла, после него их нет.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Range — форма заголовка for для перебора коллекций: for i, v := range slice сам ведёт счётчик, без len(), условия и шага.
  • По срезу и массиву: две переменные — индекс и копия значения, одна — индекс, подчёркивание — пропуск ненужного.
  • Значение из range — копия: менять элементы нужно по индексу (for i := range … prices[i] *= 2), чтение копии безопасно.
  • Range по строке даёт байтовое смещение и руну — так делает настоящий Go и основной движок песочницы; для ASCII смещения идут подряд, кириллица прыгает через два байта.
  • Итерация по карте: порядок случайный при каждом запуске (настоящий Go и основной движок песочницы); код не должен зависеть ни от какого порядка — нужен порядок, сортируйте ключи явно.
  • Range по каналу читает значения до close(ch); range по незакрытому каналу ждёт вечно — песочница оборвёт его лимитом времени. Практика — в курсе go-advanced.
  • Литерал в заголовке range законен, но привычнее объявить коллекцию в переменную; range по числу (Go 1.22) исполняет основной движок песочницы, в резервном он не поддержан.

Домашнее задание

Соберите отчёт по заказам кофейни: names := []string{"эспрессо", "капучино", "чай", "круассан"} и prices := []int{120, 180, 90, 150}. Переберите prices с индексом и печатайте «заказ: name — цена P», беря имя по индексу из names. Цены выше 150 пропускайте continue-ом — круассан в отчёт не попадёт. Аккумулятор total объявите до цикла и после него напечатайте «выручка: 360». Затем вторым циклом просуммируйте значения карты wishlist := map[string]int{"стол": 3000, "лампа": 800} и напечатайте «подарки: 3800». Помните привычку из этого урока: литералы объявляйте в переменные, а в заголовке range ставьте только имя.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Base · Range: перебор коллекций)

Вопросы и ответы по уроку

Чем range лучше классического for с len()?

Короче и без off-by-one: нет счётчика, границы и шага — нечему перепутать, и выйти за границу среза невозможно. Но классический for не заменяется полностью: шаг больше единицы, обратный отсчёт, границы, меняющиеся в теле, — это по-прежнему его территория, как в уроках 13 и 14. Range закрывает перебор по одному элементу слева направо.

Как обойти карту в отсортированном порядке?

Собрать ключи в срез, отсортировать sort.Strings из пакета sort (в песочнице работает — проверено пробником) и перебирать срез ключей, беря значения по ключу: for _, k := range keys { fmt.Println(k, m[k]) }. Порядок самой карты в настоящем Go всё равно случайный, поэтому сортировку делают явно каждый раз, когда порядок важен.

Когда range по строке может считать байты, а не руны?

В основном движке песочницы — никогда: там настоящий Go, руны со смещениями. Байтовый обход — упрощение резервного движка, который включается автоматически, только если WASM недоступен: он перебирает байты строки, и в переменной оказывается код байта 0–255. Для ASCII разницы с настоящим Go нет. Строковые примеры с кириллицей и смещениями проверяйте на основном движке или локальном компиляторе, в резервном — считайте байты.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления