· 9 мин 👁 1.3k Начинающий

Язык Go - цикл for

Один цикл вместо трёх: как for в Go заменяет for, while и do-while, что умеет range и где прячутся сюрпризы с Unicode и горутинами.

forrangeциклыunicode
Содержание

В Go один цикл — for. Без while, без do-while. Звучит как ограничение, но на практике это просто три формы одного и того же ключевого слова.

Три формы for

// Классический C-шный for: (for [init]; [condition]; [post]). 
// init — выполняется один раз перед началом цикла
// condition — проверяется перед каждой итерацией, если false, то цикл прерывается
// post — выполняется после каждой итерации
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

// Как while в других языках — только условие, без init и post
// Цикл работает пока condition == true
for condition {
    // что-то делаем, пока условие выполняется
}

// бесконечный цикл
for { }

Примеры

Пример объявления и изменения переменной:

sum := 0
for i := 0; i < 10; i++ {
    sum += i
}
// i здесь уже не существует — область видимости заканчивается вместе с циклом

while-форма

n := 1
for n < 1024 {
    n *= 2
}

Бесконечный цикл

for {
    input := readLine() // читаем следующую строку ввода
    if input == "" {
        break // пустая строка — выходим из цикла
    }
    process(input)
}

range: обход коллекций

Для массивов, слайсов, строк, map и каналов есть range. Он возвращает два значения — индекс/ключ и значение:

// Инициализируем переменные:
// map — структура данных "ключ → значение"
// map[тип ключа]тип значения{ключ: значение, ...}
oldMap := map[string]int{"a": 1, "b": 2, "c": 3}

// make создаёт пустую map, готовую к записи
// просто map[string]int{} тоже сработает
newMap := make(map[string]int)

Итерация по элементам map:

// key   — ключ текущего элемента ("a", "b", "c")
// value — значение текущего элемента (1, 2, 3)
for key, value := range oldMap { // порядок НЕ гарантирован
    newMap[key] = value // копируем пару ключ-значение в новую map
}

Если нужно получить что-то одно: или ключ или значение, то есть два варианта:

// только ключ (значение не нужно) — просто опускаем второй параметр
m := map[string]int{"a": 1, "b": 2, "c": 3}
for key := range m { // порядок не гарантирован
    fmt.Println(key) // выводим только ключи
}

// только значение (ключ не нужен) — используем пустой идентификатор _
for _, value := range m { // порядок не гарантирован
    fmt.Println(value) // выводим только значения
}

_ — это не переменная, а специальный идентификатор, который указывает компилятору, что первое возвращаемое значение не используется. Благодаря этому компилятор не будет выдавать ошибок о неиспользуемой переменной.

range по строке — осторожно, Unicode

Вот где начинается интересное. range по строке работает не побайтово, а по Unicode code points (в Go они называются rune):

for pos, char := range "日本\x80語" {
    fmt.Printf("character %#U starts at byte position %d\n", char, pos)
}

Вывод:

character U+65E5 '日' starts at byte position 0
character U+672C '本' starts at byte position 3
character U+FFFD '?' starts at byte position 6
character U+8A9E '語' starts at byte position 7

Что здесь происходит: занимает 3 байта в UTF-8 (позиции 0, 1, 2), поэтому следующий символ начинается с байта 3. \x80 — невалидная UTF-8 последовательность, range заменяет её на U+FFFD (символ замены) и «съедает» один байт. начинается с позиции 7.

Итого: pos — это байтовый индекс, не номер символа. Если нужен именно номер — заводите отдельный счётчик.

Если хотите побайтовый обход — просто индексируйте строку напрямую:

s := "hello"
for i := 0; i < len(s); i++ {
    fmt.Printf("%d: %x\n", i, s[i]) // s[i] — это byte, не rune
}

Тонкости и хитрости

Переменная цикла — одна на все итерации

До Go 1.22 классическая ловушка выглядела так:

funcs := make([]func(), 5)
for i := 0; i < 5; i++ {
    funcs[i] = func() { fmt.Println(i) }
}
for _, f := range funcs {
    f() // до Go 1.22 выводило бы 5 5 5 5 5
}

Все замыкания захватывали одну и ту же переменную i, и к моменту вызова она была равна 5. В Go 1.22 это поведение изменили — теперь каждая итерация получает свою копию i, и код работает как ожидается.

Если вы на более старой версии — фиксируется явным копированием:

for i := 0; i < 5; i++ {
    i := i // новая переменная на каждую итерацию
    funcs[i] = func() { fmt.Println(i) }
}

range по слайсу — копирует заголовок, не данные

s := []int{1, 2, 3}
for i, v := range s {
    s = append(s, v*10) // изменяем s внутри цикла
    _ = i
}
fmt.Println(s) // [1 2 3 10 20 30]

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

Удалять из map во время range — можно

В Go это явно разрешено и безопасно:

m := map[string]int{"a": 1, "b": 2, "c": 3}
// m сейчас: {"a": 1, "b": 2, "c": 3}

for key := range m {
    if key == "b" {             // допустим, хотим удалить "b"
        delete(m, key)          // удаляем прямо во время перебора
    }
}
// m после цикла: {"a": 1, "c": 3} — "b" удалена, остальные на месте

// В Go это безопасно и гарантировано спецификацией языка.
// В других языках такое обычно ломается:
// - Java: ConcurrentModificationException — программа падает с ошибкой
// - C#: InvalidOperationException: Collection was modified
// - Python: RuntimeError: dictionary changed size during iteration

В отличие от многих других языков, нет повода для волнений. Стандартная библиотека гарантирует корректность такого обхода.

range по каналу

ch := make(chan int)
go func() {
    for _, v := range []int{1, 2, 3} {
        ch <- v
    }
    close(ch)
}()

for v := range ch {
    fmt.Println(v) // 1, 2, 3
}
// цикл завершится, когда канал закрыт

range по каналу читает значения до тех пор, пока канал не закрыт. Если не закрыть — цикл зависнет навсегда. Это частая причина горутин-утечек.

Метки для вложенных циклов

break и continue по умолчанию действуют на ближайший цикл. Чтобы выйти из вложенного — используются метки:

outer:
    for i := 0; i < 5; i++ {
        for j := 0; j < 5; j++ {
            if i+j == 6 {
                break outer // выходим из обоих циклов сразу
            }
        }
    }

Встречается редко, но когда нужно — удобнее, чем флаги.

Итоги

  • for в Go — единственный цикл. Три формы: классический, while-подобный, бесконечный
  • range обходит слайсы, массивы, строки, мапы и каналы
  • _ отбрасывает ненужное значение из range — компилятор требует явности
  • range по строке идёт по Unicode rune, pos — байтовый индекс
  • Удалять из map во время range — безопасно
  • range по каналу завершается только при close(ch)
  • С Go 1.22 переменная цикла создаётся заново на каждой итерации

Следующий шаг: Switch в Go — без fall-through по умолчанию, с условиями и типами.