1. Зачем нужны циклы и почему в Go он один
Если вы хоть раз пытались «просто повторить действие 10 раз» вручную, вы знаете, как это обычно заканчивается: десять одинаковых строк, и на одиннадцатой вы ошиблись в имени переменной. Циклы — это способ сказать компьютеру: «сделай вот этот блок кода несколько раз, и давай договоримся, когда остановиться».
С точки зрения здравого смысла, цикл — это управляющая конструкция, которая повторяет тело (блок { ... }) пока выполняется некое правило. В Go это особенно приятно: цикл всего один — for. Да-да, никакого while как отдельного слова. Go как бы говорит: «Зачем вам два слова, если можно одно?».
for в Go: одна конструкция на разные задачи
Важно не пугаться фразы «единственный цикл». Это не означает, что Go «бедный» — это означает, что for умеет несколько форм записи. Но сегодня мы берём самую классическую и самую полезную для новичка: цикл со счётчиком.
В реальной жизни это выглядит как «сделай 5 раз», «пройди от 1 до N», «выведи числа от 0 до 9», «прочитай N значений». Почти все такие задачи удобно выражаются классическим for, где есть начало, условие и шаг.
2. Классический for: init; cond; post
Общая форма цикла
Эта форма — как классический бутерброд: три слоя с понятными ролями. Выглядит она так:
for init; cond; post {
// тело цикла
}
В Go такую форму вы будете видеть постоянно — даже в примерах из официальных материалов, например в статье про defer, где внутри цикла откладывают печать значений. Там как раз используется привычный счётчик i := 0; i < 4; i++, то есть ровно та конструкция, которую мы сейчас разбираем.
Теперь разложим по деталям (без нервов, всё логично).
init: подготовка перед стартом
В секции init обычно создают и инициализируют счётчик. Причём это делается один раз — перед первой проверкой условия.
Чаще всего там пишут i := 0 или i := 1. Почему иногда ноль, а иногда один — это вопрос удобства и того, как вы мысленно считаете. Компьютеру всё равно, человеку — нет. Если вы нумеруете «попытки 1..N», обычно стартуют с 1. Если вы работаете с индексами (что мы позже увидим в массивах/слайсах), часто стартуют с 0.
Мини-пример: объявили счётчик и подготовились печатать.
package main
import "fmt"
func main() {
for i := 0; i < 3; i++ {
fmt.Println("Итерация", i) // Итерация 0 / 1 / 2
}
}
Обратите внимание на деталь: i объявлен через := прямо внутри for. Это нормально и очень типично: счётчик живёт внутри цикла и почти никогда не нужен снаружи.
cond: условие остановки
cond (condition) — это выражение типа bool, которое проверяется перед каждой итерацией. Если условие истинно — выполняется тело цикла. Если ложно — цикл завершается, и программа продолжает выполнение после цикла.
Важно почувствовать простую мысль: условие — это «охранник на входе». Он не пускает вас на очередную итерацию, если правило уже не выполняется.
Пример на «выведи числа 1..5» (включительно):
package main
import "fmt"
func main() {
for i := 1; i <= 5; i++ {
fmt.Println(i) // 1 2 3 4 5
}
}
Здесь условие i <= 5 означает: «пока i не больше 5 — работаем».
Тело цикла: что именно повторяем
Тело цикла — это обычный блок кода в фигурных скобках. Вы уже видели такие блоки в if/else и switch. В цикле блок выполняется много раз — и это единственное отличие.
Хорошая привычка: внутри тела цикла держать код максимально простым. Если внутри цикла слишком много всего, становится сложно понять, сколько раз и что именно происходит. Пока мы новички — мы пишем маленькими понятными кусочками, а «большие простыни» оставим людям, которые любят страдать.
Простейший пример тела: вывести что-то несколько раз.
package main
import "fmt"
func main() {
for i := 0; i < 2; i++ {
fmt.Println("Go — это нормально") // Go — это нормально (2 раза)
}
}
post: шаг между итерациями
post выполняется после тела на каждой итерации. Это то место, где обычно меняют счётчик: увеличивают или уменьшают его, чтобы цикл в какой-то момент завершился.
Если вы забыли менять счётчик (или меняете не туда) — цикл может стать бесконечным. Но паниковать рано: это типичная ошибка новичка, и мы её научимся ловить глазами.
Мини-пример: шаг на 2.
package main
import "fmt"
func main() {
for i := 0; i < 6; i = i + 2 {
fmt.Println(i) // 0 2 4
}
}
Здесь post — это i = i + 2. Он выполняется после каждого fmt.Println.
i++ и i--: короткая запись «плюс один» и «минус один»
В Go для счётчиков есть два очень узнаваемых символа из мира «классических циклов»: i++ и i--. Это короткая запись для «увеличить на 1» и «уменьшить на 1».
Важный момент для чтения кода: когда вы видите i++, почти всегда это означает «обычный счётчик, обычная итерация».
Пример с уменьшением:
package main
import "fmt"
func main() {
for i := 3; i > 0; i-- {
fmt.Println(i) // 3 2 1
}
}
Здесь условие i > 0 и шаг i-- согласованы: мы идём вниз к нулю, и цикл гарантированно остановится.
Порядок выполнения: init → cond → body → post
Пока вы не научитесь прокручивать цикл в голове, любые for будут ощущаться как магия. Хорошая новость: магии нет — есть строгий порядок.
Сначала выполняется init (ровно один раз). Потом проверяется cond. Если cond == true, выполняется тело body. Затем выполняется post. После этого цикл возвращается к проверке cond. И так по кругу, пока условие не станет ложным.
Вот схема, которую полезно помнить (как «карту метро»):
flowchart TD
A[init: один раз] --> B{cond?}
B -- true --> C[body: тело]
C --> D[post: шаг]
D --> B
B -- false --> E[после цикла]
Если вы не любите схемы, можно держать это как таблицу:
| Элемент | Когда выполняется | Типичное содержимое |
|---|---|---|
|
один раз, в начале | |
|
перед каждой итерацией | |
|
если |
полезная работа |
|
после тела | |
3. Полезные привычки и важные нюансы
Привычка: понимать, какие значения примет i
Это супер-навык для новичка: уметь ответить на вопрос «какие значения примет счётчик». Не “примерно”, а прям конкретно. Иначе вы будете постоянно попадать в ситуацию «я ожидал 5 повторов, а получил 6».
Разберём на примере:
for i := 0; i < 5; i++ { ... }
Счётчик стартует с 0. Условие: i < 5. Значит i будет: 0, 1, 2, 3, 4. Пятёрка уже не войдёт, потому что на i == 5 условие ложно.
А вот тут:
for i := 1; i <= 5; i++ { ... }
Счётчик: 1, 2, 3, 4, 5. Здесь 5 входит, потому что условие «меньше или равно».
Это кажется мелочью, но на практике это половина успеха в циклах.
Почему i обычно объявляют прямо в for
Когда вы пишете for i := 0; ..., вы как бы говорите: «счётчик — служебная штука, пусть живёт только тут». Это помогает не засорять main лишними переменными, которые потом случайно используют не по назначению.
Ещё это делает код более “самодокументируемым”: вы открываете файл, видите цикл и сразу понимаете, где появился i и как он меняется. Никаких «а кто такой i и почему он вдруг равен 17» (хотя, конечно, иногда жизнь всё равно подкидывает сюрпризы).
init, cond, post — части одного механизма
Очень частая ошибка начинающего — воспринимать init, cond, post как три формальности: мол, «все так пишут». На самом деле это три детали одного механизма управления.
init отвечает за стартовое состояние. cond отвечает за правило «когда продолжаем». post отвечает за движение состояния к завершению. Если хоть одна часть выбрана неудачно, цикл либо сделает не то количество итераций, либо вообще «залипнет».
Супер-простая проверка здравого смысла: попробуйте ответить на два вопроса. Какая переменная меняется на каждой итерации? И как она приближает нас к моменту, когда cond станет ложным? Если на эти вопросы нет ответа — цикл подозрительный.
4. Пример: TaskBox
Чтобы не учить for просто так, давайте начнём собирать маленькое приложение, которое будет расти вместе с курсом. Назовём его TaskBox: пока это совсем простой CLI, который помогает пользователю вводить задачи и видеть их с нумерацией.
Сегодня у нас ещё нет массивов и слайсов, поэтому «хранить список задач» мы пока не умеем. Но уже можем сделать полезную вещь: повторить ввод N раз и красиво пронумеровать то, что ввели. Это уже похоже на настоящее приложение, а не на «выведи hello 10 раз».
Читаем N и печатаем номера задач
Этот подшаг помогает почувствовать «сколько раз повторится цикл» и как выглядит нумерация.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
for i := 1; i <= n; i++ {
fmt.Println("Задача №", i) // при n=3: Задача № 1 / 2 / 3
}
}
Здесь i — это человеческая нумерация. Мы специально выбрали старт с 1, чтобы вывод выглядел дружелюбно.
Повторяем ввод N раз и печатаем то, что ввели
Теперь добавим «имя задачи». Напомню: fmt.Scan читает токены, то есть для простоты пусть задача будет одним словом (например, buy_milk). Это нормально для учебного шага.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
for i := 1; i <= n; i++ {
var title string
fmt.Scan(&title)
fmt.Println(i, "-", title) // пример: 1 - buy_milk
}
}
Это уже практический паттерн: цикл управляет количеством повторов, а внутри мы делаем одинаковую операцию (читаем и печатаем).
5. Ещё один пример: таблица квадратов
Иногда полезно увидеть, что цикл — это не только «ввод/вывод», но и вычисления. Здесь мы печатаем таблицу квадратов от 1 до N. Никаких новых конструкций — только for и умножение.
package main
import "fmt"
func main() {
var n int
fmt.Scan(&n)
for i := 1; i <= n; i++ {
fmt.Println(i, "->", i*i) // при n=3: 1->1, 2->4, 3->9
}
}
Это упражнение (в смысле “пример”) особенно хорошо тем, что вы можете быстро глазами проверить корректность результата. Если на месте 3 вдруг появилось 16 — значит, вы где-то «съехали» со счётчиком.
6. Типичные ошибки при написании классического for
Ошибка №1: перепутать < и <= и получить лишнюю или недостающую итерацию.
Это не «невнимательность», это реально самая частая история. Мозг читает i < 5 и думает «ну, до пяти», а компьютер читает буквально: «строго меньше пяти». Лечится привычкой проговаривать: «последнее значение i будет …».
Ошибка №2: неправильный старт счётчика (например, начали с 0, а выводите “попытка №0”).
Технически код верный, но пользователь будет смотреть на “попытка №0” с подозрением, как на чек из магазина с “-1 товар”. Для человеческой нумерации чаще выбирают старт с 1. Для индексирования (когда оно появится позже) часто выбирают старт с 0 — но это именно “позже”, а не “везде всегда”.
Ошибка №3: шаг не согласован с условием.
Если условие i < n, а шаг i--, то вы никогда не приблизитесь к i >= n. Это как ехать на машине к финишу, но каждый раз после поворота уезжать в противоположную сторону. Компилятор не обязан спасать вас от логики — он только следит за синтаксисом.
Ошибка №4: попытка «сложить всё сразу» в одну строку цикла.
Иногда новички пытаются сделать “круто”: инициализация сложная, условие хитрое, шаг с несколькими переменными — и всё это сразу. В итоге цикл становится загадкой даже для автора через пять минут. На старте лучше держать init простым (i := 0), cond понятным (i < n) и post очевидным (i++). Красота придёт, когда мозг перестанет тратить силы на базовую механику.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ