$ sudo teach IT
МОДУЛЬ 2 · УРОК 2.5

Циклы: for во всех формах

Единственный цикл в Go заменяет for, while и бесконечный цикл — разбираемся, как он работает

В предыдущем уроке мы научили программу принимать решения с помощью if и switch. Но до сих пор каждое действие в наших программах выполнялось ровно один раз. А что, если нужно повторить что-то десять раз? Сто? Тысячу? Не писать же сто одинаковых строк! Для этого существуют циклы — конструкции, которые повторяют блок кода снова и снова, пока выполняется определённое условие. В большинстве языков есть несколько видов циклов: for, while, do-while. В Go всё проще — есть только один цикл for, но он настолько гибкий, что заменяет все остальные. Сегодня мы разберём его во всех формах и напишем две полноценные практические программы.

🔁 В Go есть только for — и это здорово

Во многих языках программирования есть несколько видов циклов. Например, в Python есть for и while, в JavaScript — for, while, do...while и for...of. Это может запутать новичка: какой цикл использовать в какой ситуации?

Создатели Go решили эту проблему элегантно: в языке есть только один цикл — for. Но этот единственный for умеет всё: он может работать как классический счётчик, как while и даже как бесконечный цикл. Три формы одного ключевого слова — просто, понятно и без лишних сложностей.

Три формы цикла for в Go

1. Классический for — с инициализацией, условием и шагом (как for в C/Java)
2. for с условием — только условие, без инициализации и шага (как while)
3. for без всего — бесконечный цикл (как while true)

Давайте разберём каждую форму подробно, с примерами.

🔢 Классический for — повторяем N раз

Это самая привычная форма цикла. Вы задаёте стартовое значение, условие продолжения и шаг (изменение переменной после каждой итерации). Выглядит так:

package main

import "fmt"

func main() {
    for i := 0; i < 5; i++ {
        fmt.Println("Привет!", i)
    }
}

Вывод программы:

Привет! 0
Привет! 1
Привет! 2
Привет! 3
Привет! 4

Давайте разберём эту строку по частям: for i := 0; i < 5; i++

i := 0 — инициализация. Создаём переменную i и даём ей стартовое значение 0. Выполняется один раз, перед началом цикла.
i < 5 — условие. Перед каждой итерацией Go проверяет: i меньше 5? Если да — выполняем тело цикла. Если нет — цикл останавливается.
i++ — шаг (пост-действие). После каждой итерации i увеличивается на 1.

Проследим, как работает цикл шаг за шагом:

  1. Инициализация: i = 0
  2. Проверка: 0 < 5? Да. Выводим «Привет! 0». Шаг: i становится 1.
  3. Проверка: 1 < 5? Да. Выводим «Привет! 1». Шаг: i становится 2.
  4. Проверка: 2 < 5? Да. Выводим «Привет! 2». Шаг: i становится 3.
  5. Проверка: 3 < 5? Да. Выводим «Привет! 3». Шаг: i становится 4.
  6. Проверка: 4 < 5? Да. Выводим «Привет! 4». Шаг: i становится 5.
  7. Проверка: 5 < 5? Нет. Цикл завершается.

Итого 5 итераций: от 0 до 4. Обратите внимание — мы начали с нуля, а не с единицы. В программировании принято считать с нуля, и вы к этому быстро привыкнете.

💡 Подсказка: как и в if, в цикле for круглые скобки вокруг конструкции не нужны. Пишем for i := 0; i < 5; i++, а не for (i := 0; i < 5; i++). Фигурные скобки обязательны, и открывающая скобка { должна быть на той же строке.

Переменная i, объявленная в цикле, существует только внутри этого цикла. За его пределами она недоступна — точно так же, как переменная в if с инициализацией.

Шаг не обязан увеличивать переменную на 1. Вы можете считать с любым шагом и даже в обратном направлении:

package main

import "fmt"

func main() {
    // Считаем чётные числа от 0 до 10
    fmt.Println("Чётные числа:")
    for i := 0; i <= 10; i += 2 {
        fmt.Println(i)
    }

    // Обратный отсчёт
    fmt.Println("\nОбратный отсчёт:")
    for i := 5; i > 0; i-- {
        fmt.Println(i)
    }
    fmt.Println("Старт!")
}

В первом цикле i += 2 означает «увеличить i на 2 каждый раз». Получаем: 0, 2, 4, 6, 8, 10. Во втором цикле i-- уменьшает i на 1, и мы идём от 5 до 1.

🔄 for с условием — замена while

Во многих языках есть цикл while: он повторяет код, пока условие истинно, без счётчика. В Go while нет как отдельного слова, но for прекрасно его заменяет. Просто уберите инициализацию и шаг — оставьте только условие:

package main

import "fmt"

func main() {
    n := 1

    for n <= 100 {
        n *= 2  // умножаем n на 2 каждую итерацию
    }

    fmt.Println("Первая степень двойки больше 100:", n)
}

Вывод: Первая степень двойки больше 100: 128

Здесь нет ни инициализации внутри for, ни шага. Просто for n <= 100 — «пока n меньше или равно 100, продолжай». Переменная n объявлена до цикла и изменяется внутри тела. Проследим:

  1. n = 1. Проверка: 1 <= 100? Да. n = 1 * 2 = 2.
  2. n = 2. Проверка: 2 <= 100? Да. n = 2 * 2 = 4.
  3. n = 4 → 8 → 16 → 32 → 64 → 128.
  4. n = 128. Проверка: 128 <= 100? Нет. Цикл завершён.

Вот ещё один пример — вычисление суммы чисел от 1 до 100:

package main

import "fmt"

func main() {
    sum := 0
    i := 1

    for i <= 100 {
        sum += i  // прибавляем i к сумме
        i++       // увеличиваем i на 1
    }

    fmt.Println("Сумма чисел от 1 до 100:", sum)
}

Результат: Сумма чисел от 1 до 100: 5050. Знаменитая формула, которую юный Гаусс вычислил за секунды, а наша программа — за доли миллисекунды. Конечно, то же самое можно записать и классическим for: for i := 1; i <= 100; i++. Какую форму выбрать — дело вкуса и контекста. Если у вас есть чёткий счётчик, классический for обычно читается лучше.

♾️ Бесконечный цикл — for без условия

Третья форма — самая короткая. Просто for без ничего:

for {
    // этот код будет выполняться бесконечно
}

Это эквивалент while (true) из других языков. Цикл будет работать вечно, пока программу не остановят или пока внутри не сработает break (о нём — через минуту).

Звучит опасно? На самом деле бесконечные циклы используются очень часто. Сервер, который слушает запросы, работает в бесконечном цикле. Игра, которая ждёт действий пользователя, тоже. Главное — правильно из него выходить.

package main

import "fmt"

func main() {
    count := 0

    for {
        if count >= 3 {
            break  // выходим из цикла
        }
        fmt.Println("Итерация:", count)
        count++
    }

    fmt.Println("Цикл завершён")
}

Вывод:

Итерация: 0
Итерация: 1
Итерация: 2
Цикл завершён

Здесь цикл формально бесконечный, но внутри стоит проверка: если count достиг 3, срабатывает break. Это ключевое слово немедленно прерывает цикл и передаёт управление коду после закрывающей скобки }.

⛔ break и continue — управление изнутри

Мы уже видели break — он полностью прерывает цикл. Есть ещё одно важное ключевое слово — continue. Оно не останавливает цикл, а пропускает текущую итерацию и переходит к следующей.

package main

import "fmt"

func main() {
    for i := 1; i <= 10; i++ {
        if i%3 == 0 {
            continue  // пропускаем числа, делящиеся на 3
        }
        fmt.Println(i)
    }
}

Вывод:

1
2
4
5
7
8
10

Числа 3, 6 и 9 пропущены. Когда i делится на 3 без остатка, continue срабатывает, и fmt.Println(i) не выполняется. Но цикл не прекращается — он просто переходит к следующей итерации.

Давайте посмотрим пример, где break и continue используются вместе:

package main

import "fmt"

func main() {
    // Ищем первое число от 1 до 100, которое делится
    // и на 7, и на 3
    for i := 1; i <= 100; i++ {
        if i%7 != 0 {
            continue  // не делится на 7 — пропускаем
        }
        if i%3 != 0 {
            continue  // не делится на 3 — пропускаем
        }
        fmt.Println("Найдено:", i)
        break  // нашли — останавливаем цикл
    }
}

Вывод: Найдено: 21. Программа перебирает числа от 1 до 100. Сначала проверяет делимость на 7 — если нет, continue. Потом делимость на 3 — если нет, тоже continue. Если оба условия пройдены, число подходит: выводим его и делаем break.

💡 Подсказка: break и continue работают только с ближайшим циклом, в котором они находятся. Если у вас цикл внутри цикла (вложенные циклы), break выйдет только из внутреннего. Для выхода из внешнего цикла нужны метки — о них чуть ниже.

🪆 Вложенные циклы

Циклы можно вкладывать друг в друга. Внешний цикл выполняется один раз, а внутренний — полностью при каждой итерации внешнего. Это удобно, когда нужно работать с двумерными данными — например, с таблицами или координатами.

package main

import "fmt"

func main() {
    for i := 1; i <= 3; i++ {
        for j := 1; j <= 3; j++ {
            fmt.Printf("(%d,%d) ", i, j)
        }
        fmt.Println()  // новая строка после каждой строки таблицы
    }
}

Вывод:

(1,1) (1,2) (1,3)
(2,1) (2,2) (2,3)
(3,1) (3,2) (3,3) 

Внешний цикл (i) делает 3 шага. На каждом шаге внутренний цикл (j) тоже делает 3 шага. Итого получается 3 × 3 = 9 выводов. Мы используем fmt.Printf вместо fmt.Println, чтобы выводить пары чисел на одной строке, а fmt.Println() без аргументов просто переносит строку.

🏷️ Метки — выход из вложенных циклов

Представьте: у вас два вложенных цикла, и во внутреннем вы нашли нужное значение. Вы хотите остановить оба цикла. Но break остановит только внутренний. Что делать?

В Go есть метки (labels). Метка — это имя, которое вы даёте циклу. Потом можно написать break ИмяМетки — и выйти из нужного цикла, даже если вы глубоко вложены.

package main

import "fmt"

func main() {
outer:
    for i := 1; i <= 5; i++ {
        for j := 1; j <= 5; j++ {
            if i*j > 10 {
                fmt.Printf("Прерываем: i=%d, j=%d, произведение=%d\n", i, j, i*j)
                break outer  // выходим из ОБОИХ циклов
            }
            fmt.Printf("%d×%d=%d  ", i, j, i*j)
        }
        fmt.Println()
    }
    fmt.Println("Оба цикла завершены")
}

Вывод:

1×1=1  1×2=2  1×3=3  1×4=4  1×5=5
2×1=2  2×2=4  2×3=6  2×4=8  2×5=10
3×1=3  3×2=6  3×3=9  3×4=12  Прерываем: i=3, j=4, произведение=12
Оба цикла завершены

Разберём по порядку. Строка outer: — это метка. Она ставится перед циклом, на отдельной строке (или на той же строке, что и for). Имя метки может быть любым, но принято писать в нижнем регистре. Когда программа доходит до break outer, она выходит не из внутреннего цикла, а из того цикла, который помечен меткой outer — то есть из внешнего.

continue тоже работает с метками. continue outer пропустит текущую итерацию не внутреннего, а внешнего цикла:

package main

import "fmt"

func main() {
outer:
    for i := 1; i <= 3; i++ {
        for j := 1; j <= 3; j++ {
            if j == 2 {
                continue outer  // переходим к следующей итерации внешнего цикла
            }
            fmt.Printf("i=%d, j=%d\n", i, j)
        }
    }
}

Вывод:

i=1, j=1
i=2, j=1
i=3, j=1

Для каждого значения i внутренний цикл начинается с j=1, но когда j становится 2, continue outer немедленно переходит к следующей итерации внешнего цикла. Поэтому j=2 и j=3 мы никогда не видим.

⚠️ Типичные ошибки новичков

Перед практикой разберём частые ловушки, в которые попадают начинающие.

Ошибка 1: бесконечный цикл по ошибке

// ОПАСНО — цикл никогда не закончится!
i := 0
for i < 10 {
    fmt.Println(i)
    // Забыли i++ — переменная не меняется!
}

Если переменная в условии не изменяется, условие всегда истинно и цикл работает вечно. Программа зависнет. Чтобы остановить зависшую программу, нажмите Ctrl+C в терминале.

Ошибка 2: ошибка на единицу (off-by-one)

// Хотим вывести числа от 1 до 5
for i := 1; i < 5; i++ {
    fmt.Println(i)  // выведет 1, 2, 3, 4 — без 5!
}

// Правильно:
for i := 1; i <= 5; i++ {
    fmt.Println(i)  // выведет 1, 2, 3, 4, 5 ✓
}

Разница между < и <= критична. Всегда проверяйте: нужно ли включать последнее значение.

Ошибка 3: изменение счётчика внутри тела

// ПЛОХАЯ ПРАКТИКА — запутанное поведение
for i := 0; i < 10; i++ {
    i += 2  // меняем i ещё и внутри тела
    fmt.Println(i)
}

💡 Подсказка: старайтесь не изменять переменную-счётчик внутри тела цикла. Это делает код трудным для понимания. Если вам нужен нестандартный шаг — просто измените третью часть for (например, i += 3).

🛠 Практика: таблица умножения

Теперь применим все знания на практике. Напишем программу, которая выводит таблицу умножения от 1 до 9. Это классическая задача на вложенные циклы.

package main

import "fmt"

func main() {
    fmt.Println("=== ТАБЛИЦА УМНОЖЕНИЯ ===")
    fmt.Println()

    // Заголовок — номера столбцов
    fmt.Print("     ")
    for j := 1; j <= 9; j++ {
        fmt.Printf("%4d", j)
    }
    fmt.Println()

    // Разделительная линия
    fmt.Print("    ")
    for j := 1; j <= 9; j++ {
        fmt.Print("----")
    }
    fmt.Println()

    // Таблица
    for i := 1; i <= 9; i++ {
        fmt.Printf("%2d |", i)  // номер строки
        for j := 1; j <= 9; j++ {
            fmt.Printf("%4d", i*j)
        }
        fmt.Println()
    }
}

Вывод:

=== ТАБЛИЦА УМНОЖЕНИЯ ===

        1   2   3   4   5   6   7   8   9
    ------------------------------------
 1 |   1   2   3   4   5   6   7   8   9
 2 |   2   4   6   8  10  12  14  16  18
 3 |   3   6   9  12  15  18  21  24  27
 4 |   4   8  12  16  20  24  28  32  36
 5 |   5  10  15  20  25  30  35  40  45
 6 |   6  12  18  24  30  36  42  48  54
 7 |   7  14  21  28  35  42  49  56  63
 8 |   8  16  24  32  40  48  56  64  72
 9 |   9  18  27  36  45  54  63  72  81

Разберём программу.

Заголовок. Первый цикл выводит номера столбцов. fmt.Printf("%4d", j) форматирует число так, чтобы оно занимало 4 символа — это обеспечивает ровные столбцы. Если число короткое (например, 1), перед ним будут пробелы.

Разделительная линия. Второй цикл рисует линию из дефисов для красоты.

Тело таблицы. Два вложенных цикла. Внешний (i) перебирает строки от 1 до 9. В начале каждой строки выводим номер строки с разделителем |. Внутренний (j) перебирает столбцы от 1 до 9 и выводит произведение i*j. После каждой строки fmt.Println() переносит курсор на новую строку.

Задание для самостоятельной работы

Попробуйте модифицировать программу: выведите только верхний треугольник таблицы — то есть, если j < i, выводите пробелы вместо числа. Подсказка: вам понадобится if внутри внутреннего цикла.

🎮 Практика: игра «Угадай число»

Теперь напишем полноценную интерактивную программу! Компьютер загадает случайное число от 1 до 100, а вы будете его угадывать. После каждой попытки программа подсказывает — загаданное число больше или меньше. Здесь мы используем бесконечный цикл, break, ввод от пользователя и условия.

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // Инициализируем генератор случайных чисел
    rand.Seed(time.Now().UnixNano())

    // Загадываем число от 1 до 100
    secret := rand.Intn(100) + 1

    fmt.Println("🎲 Я загадал число от 1 до 100. Попробуй угадать!")
    fmt.Println()

    attempts := 0  // счётчик попыток

    for {
        var guess int
        fmt.Print("Твоё число: ")
        fmt.Scan(&guess)

        attempts++

        if guess < 1 || guess > 100 {
            fmt.Println("Введи число от 1 до 100!")
            continue
        }

        if guess < secret {
            fmt.Println("⬆️  Моё число БОЛЬШЕ. Попробуй ещё!")
        } else if guess > secret {
            fmt.Println("⬇️  Моё число МЕНЬШЕ. Попробуй ещё!")
        } else {
            fmt.Printf("🎉 Верно! Ты угадал число %d за %d попыток!\n", secret, attempts)
            break
        }
    }

    // Оценка результата
    fmt.Println()
    switch {
    case attempts <= 3:
        fmt.Println("🏆 Невероятно! Ты настоящий телепат!")
    case attempts <= 7:
        fmt.Println("👍 Отлично! Очень хороший результат!")
    case attempts <= 10:
        fmt.Println("👌 Хорошо! Неплохая стратегия.")
    default:
        fmt.Println("💪 Ничего, с практикой станет лучше!")
    }
}

Давайте разберём программу по частям.

Импорт пакетов. Мы подключаем три пакета: fmt для ввода-вывода, math/rand для генерации случайных чисел и time для получения текущего времени (нужно для инициализации генератора).

Генерация числа. rand.Seed(time.Now().UnixNano()) инициализирует генератор случайных чисел текущим временем. Без этой строки генератор каждый раз выдавал бы одну и ту же последовательность чисел. rand.Intn(100) возвращает случайное число от 0 до 99, поэтому прибавляем 1, чтобы получить диапазон от 1 до 100.

Бесконечный цикл. for { ... } — игра продолжается, пока игрок не угадает число. Внутри цикла мы:

  1. Объявляем переменную guess для хранения ответа игрока.
  2. Читаем число с клавиатуры через fmt.Scan(&guess). Символ & перед guess означает «адрес переменной» — так Scan знает, куда записать введённое значение. Подробнее об этом мы поговорим позже.
  3. Увеличиваем счётчик попыток attempts++.
  4. Проверяем корректность ввода: если число вне диапазона, continue пропускает остальные проверки и запрашивает ввод снова.
  5. Сравниваем guess с secret и выводим подсказку. Если угадали — break завершает цикл.

Оценка результата. После цикла используем switch без условия, чтобы оценить количество попыток. Обратите внимание, как красиво сочетаются все конструкции, которые мы изучили: цикл, условия, break, continue и switch.

Запустите программу:

go run main.go

Пример игры:

🎲 Я загадал число от 1 до 100. Попробуй угадать!

Твоё число: 50
⬆️  Моё число БОЛЬШЕ. Попробуй ещё!
Твоё число: 75
⬇️  Моё число МЕНЬШЕ. Попробуй ещё!
Твоё число: 62
⬆️  Моё число БОЛЬШЕ. Попробуй ещё!
Твоё число: 68
⬇️  Моё число МЕНЬШЕ. Попробуй ещё!
Твоё число: 65
🎉 Верно! Ты угадал число 65 за 5 попыток!

👍 Отлично! Очень хороший результат!

💡 Подсказка: оптимальная стратегия — бинарный поиск. Каждый раз загадывайте середину оставшегося диапазона. Так вы угадаете любое число от 1 до 100 максимум за 7 попыток! Попробуйте сами и проверьте.

📊 Три формы for — сводная таблица

Подведём итог. Вот все формы for в одном месте:

Классический for (замена for из C/Java)

for i := 0; i < n; i++ {
    // повторяем n раз
}

for с условием (замена while)

for condition {
    // повторяем, пока условие истинно
}

Бесконечный for (замена while true)

for {
    // повторяем вечно (выход через break)
}

📋 Итоги урока

  • В Go есть только один цикл — for, но он заменяет for, while и do-while из других языков
  • Классический for: for i := 0; i < n; i++ { ... } — для повторения заданное количество раз
  • Цикл-while: for condition { ... } — повторяет, пока условие истинно
  • Бесконечный цикл: for { ... } — работает вечно, выход через break
  • break полностью прерывает цикл, continue пропускает текущую итерацию
  • Метки позволяют break и continue работать с внешним циклом при вложенных циклах
  • Мы написали таблицу умножения (вложенные циклы) и интерактивную игру «Угадай число» (бесконечный цикл + break + continue)

В следующем уроке мы изучим функции — один из самых важных инструментов в программировании. Функции позволяют разбивать код на логические блоки, давать им имена и переиспользовать. Вы узнаете, как объявлять функции, передавать аргументы и возвращать результаты. Это фундамент, без которого невозможно писать серьёзные программы!