Содержание урока · 11 разделов
- Range go: перебор без ручного счётчика
- Range по срезу go: индекс, значение и подчёркивание
- Range по строке: байтовые смещения и руны
- Итерация по карте: порядок никто не обещал
- Range по каналу — коротко и честно
- Индекс или значение: когда что выбирать
- Массивы, структуры и range без переменных
- Шпаргалка: что даёт range для каждой коллекции
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
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
GOpackage 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 без второй переменной. В песочнице разница не видна, но привычка дешёвая: перебираете тяжёлые структуры — берите индекс. Для обычных же срезов две переменные читаются лучше, и экономия на букве не стоит ясности.
Изменение среза на месте
GOpackage 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.
Песочница работает на двух движках. Основной — настоящий Go: интерпретатор yaegi, скомпилированный в WebAssembly; всё, что разобрано в уроке, включая кириллицу-руны, close() и range по числам, он исполняет как настоящий компилятор, а порядок карты у него случайный при каждом запуске. Резервный упрощённый интерпретатор включается автоматически, только если основной недоступен, и у него есть упрощения: порядок карты отсортирован (числовые ключи — как строки, то есть 1, 10, 2), range по строке идёт по байтам, метки и close не поддерживаются, range по числу не поддержан, литерал коллекции в заголовке range не парсится — поэтому в уроке коллекции всегда объявлены в переменные. Каждый ранимый пример урока прогнан пробником и даёт одинаковый вывод в обоих движках. Страховки вечных циклов мы разбирали в уроке 13 — для range они не нужны: перебор всегда конечен.
Вставьте пропуски: сумма цен
Соберите суммирование цен: переберите срез, игнорируя индекс, и накопите итог в аккумуляторе, объявленном до цикла. Правильная программа печатает «итого: 450».
Соберите программу: перебор языков
Соберите перебор среза языков: объявление среза в переменную, range с индексом и именем, печать каждой пары и финальная строка после цикла. Правильная программа печатает «0 go», «1 rust», «2 python» и «перебор закончен».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Шпаргалка: что даёт range для каждой коллекции#
| Коллекция | Одна переменная | Две переменные | Порядок обхода |
|---|---|---|---|
| Срез, массив | индекс | индекс и значение (копия) | по индексам слева направо |
| Строка | индекс байта | байтовое смещение и руна | по рунам слева направо; в резервном движке — по байтам |
| Карта | ключ | ключ и значение | случайный при каждом запуске; в резервном движке — отсортированный |
| Канал | — | значение | порядок поступления; читает до close |
Карточки: range и коллекции
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 ставьте только имя.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем 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: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления