Язык Go - цикл for
Один цикл вместо трёх: как for в Go заменяет for, while и do-while, что умеет range и где прячутся сюрпризы с 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 по умолчанию, с условиями и типами.