---
title: "Range: перебор коллекций"
description: "Range в Go: перебор среза с индексом и значением, итерация по карте, range по строке и каналу. Разбираем _, порядок карт и ловушки — с примерами в песочнице."
course: "Go с нуля: базовый синтаксис и конструкции"
url: "https://golangmaster.ru/courses/go-basics/lessons/15-range/"
published: "2026-09-19"
updated: "2026-09-28"
minutes: 20
---

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

*Курс «Go с нуля: базовый синтаксис и конструкции», урок 15 из 25. ~20 минут. Обновлено 28.09.2026. Источник: https://golangmaster.ru/courses/go-basics/lessons/15-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 по каналу. Плюс два вопроса, которые решают всё: индекс или значение брать в цикл, и куда девать ненужную переменную.

### 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](/courses/go-basics/lessons/14-while-break-continue/). Переменные i и v живут только внутри цикла — после него их не существует, это та же дисциплина областей видимости, что у счётчика классического for. Запустим первый перебор.

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

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

```go
package main

import "fmt"

func main() {
        fruits := []string{"яблоко", "груша", "слива"}
        for i, v := range fruits {
                fmt.Println(i, "—", v)
        }
        fmt.Println("полка обойдена")
}
```

### 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) }. Что напечатает программа?

- Значения: 10, 20 и 30
- ✅ Индексы: 0, 1 и 2
- Нули: v не привязана к срезу
- Ошибку: range требует ровно две переменные

Единственная переменная в range по срезу получает индекс, а не значение — по этому правилу работают и срезы, и массивы. Для значений нужна вторая переменная (for i, v := range) или подчёркивание на месте индекса: for _, v := range prices. Ошибки не будет — форма с одной переменной законна и полезна для записи по индексу.

### 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), а блок обязан вести себя одинаково в любом режиме. Руны проверьте сами: вставьте этот код в [песочницу](/playground/) и запустите на основном движке.

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

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

```go
package main

import "fmt"

func main() {
        count := 0
        for i, b := range "golang" {
                count++
                fmt.Println(i, b)
        }
        fmt.Println("оборотов:", count, "байт:", len("golang"))
}
```

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

```go
package main

import "fmt"

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

**Что выведет программа?**

- ✅ 0 104 и 1 105: смещение и код каждого символа
- 0 104 и 2 105: смещение прыгает через байт
- h и i: range печатает сами символы
- 0 104, 1 105 и 2 0: в конце добавляется нулевой байт

"hi" — чистый ASCII: один байт на символ, поэтому смещения идут подряд 0 и 1, а в b попадают коды 104 и 105 — это 'h' и 'i' в таблице ASCII. На кириллице пары разъехались бы: в настоящем Go и основном движке песочницы — по рунам со смещениями 0, 2, 4…, в резервном упрощённом — по байтам, но для латиницы оба поведения совпадают, и такой вывод даст и песочница, и локальный компилятор.

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

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

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

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

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

```go
package main

import (
        "fmt"
        "sort"
)

func main() {
        prices := map[string]int{"хлеб": 45, "молоко": 89, "сыр": 520}
        names := []string{}
        for name := range prices {
                names = append(names, name)
        }
        sort.Strings(names) // порядок карты не обещан — сортируем явно
        total := 0
        for _, name := range names {
                total += prices[name]
                fmt.Println(name, prices[name])
        }
        fmt.Println("итого:", total)
}
```

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

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

- Одинаковый в обоих запусках: порядок карты фиксирован вставкой
- ✅ Случайный при каждом запуске: язык намеренно перемешивает порядок
- Алфавитный: строки всегда обходятся по алфавиту
- Обратный: от последнего вставленного ключа к первому

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

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

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

В основном движке песочницы этот цикл прогнать можно: close(ch) там есть, и range по закрытому каналу напечатает задачи и спокойно выйдет из цикла. Опасность другая — range по незакрытому каналу блокируется навсегда, как только данные кончились: основной движок оборвёт такое ожидание лимитом времени («превышен лимит времени»), а в резервном упрощённом движке close вовсе нет — на close(ch) он отвечает «неопределённая переменная close», и range по открытому каналу зависает без всякого сообщения. Поэтому живые канал-примеры — в [курсе go-advanced](/courses/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]. Второй цикл — образец противоположной задачи: значение нужно только для чтения, индекс спрятан в подчёркивание, аккумулятор объявлен до цикла и печатается после. Две формы рядом — и видно, как выбор переменных в заголовке сам документирует намерение.

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

```go
package main

import "fmt"

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

**Что выведет программа?**

- [10 20 30]: умножение изменило каждый элемент
- ✅ [1 2 3]: v — копия, срез не тронут
- [1 2 3 10 20 30]: умноженные значения добавились в конец
- Ошибка: присваивать переменной из range нельзя

Значение из range — копия: v = v * 10 меняет копию, которая умирает на следующем обороте, и срез остаётся [1 2 3]. Присваивать в v можно — ошибки нет, просто эффект остаётся внутри тела. Чтобы срез изменился, запись идёт по индексу: for i := range nums и nums[i] *= 10, как в блоке выше.

### Массивы, структуры и 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](/courses/go-basics/).

> **Честно о песочнице: два движка.** Песочница работает на двух движках. Основной — настоящий Go: интерпретатор yaegi, скомпилированный в WebAssembly; всё, что разобрано в уроке, включая кириллицу-руны, close() и range по числам, он исполняет как настоящий компилятор, а порядок карты у него случайный при каждом запуске. Резервный упрощённый интерпретатор включается автоматически, только если основной недоступен, и у него есть упрощения: порядок карты отсортирован (числовые ключи — как строки, то есть 1, 10, 2), range по строке идёт по байтам, метки и close не поддерживаются, range по числу не поддержан, литерал коллекции в заголовке range не парсится — поэтому в уроке коллекции всегда объявлены в переменные. Каждый ранимый пример урока прогнан пробником и даёт одинаковый вывод в обоих движках. Страховки вечных циклов мы разбирали в уроке 13 — для range они не нужны: перебор всегда конечен.

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

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

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

Подсказка: Первый пропуск — ключевое слово range-заголовка между именем переменной и коллекцией. Второй — оператор «сложить и присвоить», он же сокращение для total = total + price.

Правильные ответы в пропусках: `range`, `+=`

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

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

Правильный порядок строк:

1. `package main`
2. `import "fmt"`
3. `func main() {`
4. `langs := []string{"go", "rust", "python"}`
5. `for i, name := range langs {`
6. `fmt.Println(i, name)`
7. `}`
8. `fmt.Println("перебор закончен")`
9. `}`

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

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

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

- **Как выглядит range по срезу и что дают его переменные?** — for i, v := range slice: i — индекс, v — копия значения. Одна переменная — индекс; подчёркивание пропускает ненужное. Индекс и значение видны только внутри цикла, после него их нет.
- **Как изменить элементы среза, если перебираешь через range?** — Через v — никак: это копия. Пишите for i := range и меняйте по индексу: prices[i] *= 2. Чтение через копию допустимо всегда, запись — только по индексу.
- **Что даёт range по строке в настоящем Go?** — i — байтовое смещение начала руны, r — код символа. ASCII: смещения 0, 1, 2… Кириллица в UTF-8 занимает два байта — смещения 0, 2, 4…, а r — целая буква. len() той же строки вернёт байты.
- **Что даёт range по строке с кириллицей?** — В настоящем Go и основном движке песочницы — руны со смещениями: «привет» — 6 оборотов, смещения 0, 2, 4…, в переменной целая буква. Резервный упрощённый движок идёт по байтам — 12 оборотов с кодами 0–255. Для ASCII оба одинаковы.
- **Какой порядок у итерации по карте?** — Случайный при каждом запуске — и в настоящем Go, и в основном движке песочницы: язык намеренно мешает порядок, чтобы код от него не зависел (отсортированный порядок — упрощение только резервного движка). Нужен порядок: ключи в срез, sort.Strings, range по срезу.
- **Что читает range по каналу?** — Значения, пока канал не закрыт close(ch) — сигнал «данных больше не будет». Range по незакрытому каналу ждёт вечно: основной движок песочницы прервёт его лимитом времени. Практика с close и горутинами — в курсе go-advanced.

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

- 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 нет. Строковые примеры с кириллицей и смещениями проверяйте на основном движке или локальном компиляторе, в резервном — считайте байты.
