Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~20 мин +50 XP

Цикл for: классическая форма

До сих пор каждая строка наших программ исполнялась максимум один раз: условия [if и else](/courses/go-basics/lessons/11-usloviya-if/) и [switch](/courses/go-basics/lessons/12-switch/) выбирали дорогу, но шли по ней единожды. Настоящие программы повторяют действия: разобрать тысячу строк лога, начислить процент за каждый месяц, проверить каждую строку формы. Для повторов в Go есть ровно один оператор — цикл for: счётчик, условие и шаг собраны в одном заголовке. Опуская части заголовка, из него достаются и «пока»-циклы, и вечные циклы — других циклов в языке нет, и это осознанное решение. Здесь разберём классическую форму: как читается заголовок, сколько раз выполнится тело, где прячется ловушка off-by-one и как собрать таблицу умножения двумя вложенными циклами.

Содержание урока · 10 разделов

Циклы в golang: почему цикл один#

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

Языки щедры на циклы: где-то есть while, где-то do-while, repeat-until, foreach по коллекции. В Go из этого набора существует только for — и это не экономия, а расчёт на один хорошо продуманный инструмент. Заголовок for собран из трёх частей через точку с запятой, и, опуская части, из него достаются все остальные формы: оставить условие — получится «пока», убрать всё — вечный цикл. Один оператор вместо четырёх: меньше синтаксиса, меньше споров в ревью, одинаковые циклы в коде всей команды.

Классическая форма, с которой начинается любой счёт, выглядит так: for i := 1; i <= 5; i++ { fmt.Println(i) } — счётчик, условие и шаг в одном заголовке. Ниже прогоним её в песочнице, посчитаем обороты, соберём таблицу умножения из двух вложенных циклов и разберём ошибки на единицу — off-by-one, самую частую ловушку начинающих.

Песочница: разминка со счётчиком

Запустите: счётчик пробежит от 1 до 5, и после цикла напечатается финальная строка. Поменяйте старт на i := 3 и условие на i <= 8 — счёт пойдёт с трёх по восемь. Затем сделайте обратный отсчёт: i := 5; i > 0; i--, не забудьте про минус в шаге. Напоследок допишите после цикла fmt.Println(i) и запустите: песочница ответит отказом — об этом разговор ниже, в разделе про область видимости.

Вывод

Запустите код, чтобы увидеть вывод.

Цикл for go: счётчик, условие и шаг#

Разложим заголовок for i := 1; i <= 5; i++ на три части. Первая — инициализация: i := 1 создаёт счётчик и выполняется ровно один раз, перед первым оборотом. Вторая — условие: i <= 5 проверяется перед каждым оборотом; истинно — тело работает, ложно — цикл заканчивается. Третья — шаг: i++ срабатывает после каждого оборота тела и готовит счётчик к следующей проверке.

Читается заголовок одной фразой: «начни с единицы; пока счётчик не больше пяти; после каждого оборота прибавляй единицу». Порядок работы строгий: инициализация, проверка, тело, шаг, снова проверка, снова тело и шаг — и так до первого ложного условия. Условие — обычное булево выражение из сравнений и логики, которые мы собирали в уроке про bool и логику; ложь в нём завершает цикл так же спокойно, как завершала развилку в if.

Форма повторяет привычки if: круглых скобок вокруг заголовка нет — gfmt, точнее gofmt, их удалит, фигурные скобки обязательны даже для одной строки тела. А i++ и i += 2 мы уже разбирали в уроке про операторы и выражения — здесь они работают шагом цикла. Напомню деталь, которая там же и объяснялась: i++ — самостоятельный оператор, а не выражение, в арифметику его не вставить.

Проверьте себя: сколько раз выполнится тело

Перед вами цикл for i := 0; i < 3; i++ { fmt.Println(i) }. Сколько строк напечатает программа?

Off-by-one: сколько оборотов сделает цикл#

Вопрос «сколько раз выполнится тело» стоит задавать себе перед каждым запуском — и отвечать подстановкой, а не ощущением. В for i := 1; i <= 5; i++ счётчик примет значения 1, 2, 3, 4, 5 — пять оборотов. В for i := 0; i < 5; i++ — значения 0, 1, 2, 3, 4, и снова пять. Обе записи честно дают пять оборотов, но границы у них разные: первая про «от одного до пяти», вторая — про «пять штук начиная с нуля». Смешение стилей и рождает ошибки на единицу: напишете i <= 5 со стартом с нуля — получите шесть оборотов, i < 5 со стартом с единицы — четыре.

Правило, которое экономит часы отладки: проверяйте первое и последнее значение счётчика, а не считайте обороты в уме. Подставьте стартовое значение в условие — оно обязано быть истинным. Возьмите значение после последнего шага — оно обязано быть ложным. Две подстановки глазами — и off-by-one поймана до запуска, не дожидаясь тестов и жалоб пользователей на «потерянную последнюю строку».

Шаг, впрочем, не обязан быть единицей: i += 2 перескакивает через один номер, i-- гонит счётчик вниз, стартовое значение бывает каким угодно. Это всё тот же цикл — меняется только траектория счётчика; в песочнице ниже шагаем через два.

Песочница: шаг через два

Запустите: шаг += 2 перескакивает через номер, поэтому бронь получат только нечётные места. Смените старт на seat := 2 — пойдут чётные. Разверните обход: seat := 9; seat > 0; seat -= 2 — и сверьте, что места печатаются от дальнего к ближнему. Затем поставьте шаг seat += 3 и до запуска прикиньте по пальцам, какие места забронируются, — проверьте себя выводом песочницы.

Вывод

Запустите код, чтобы увидеть вывод.

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

Прочитайте код и выберите, что он выведет:

package main

import "fmt"

func main() {
        total := 0
        for i := 1; i <= 3; i++ {
                total += i * 2
        }
        fmt.Println(total)
}

Накопление: аккумулятор за пределами цикла#

Половина практических задач с циклами — накопление: сложить числа, собрать строку, посчитать итог. Паттерн один и тот же: переменную объявляют до цикла — sum := 0; в теле наращивают — sum += i; после цикла используют. Счётчик i умирает вместе с циклом, аккумулятор переживает его — поэтому и место ему снаружи, на уровне функции: это та же дисциплина областей видимости, которую мы разбирали в уроке про переменные и константы.

Классическая ошибка — объявить аккумулятор внутри тела. sum := 0 в фигурных скобках обнуляется на каждом обороте, и после цикла в переменной остаётся только добавка последнего оборота, а не сумма. Если программа после цикла печатает не то число, первым делом проверьте, где объявлен аккумулятор.

Работает приём не только с числами: acc += "го " собирает строку из кусочков — так накапливают логи, разметку, строки отчётов. Механика прежняя: объявить до цикла, наращивать в теле, использовать после.

Вставьте части заголовка

Соберите заголовок классического цикла: счётчик идёт от единицы до трёх включительно и после каждого оборота растёт на единицу. Готовый цикл должен трижды напечатать строку «итерация» с номером.

i := 1; i 3; i { fmt.Println("итерация", i) }

Песочница: таблица умножения

Запустите: на каждый оборот внешнего цикла внутренний успевает прокрутиться целиком, поэтому строки идут тройками. Расширьте оба цикла до девяти — получится полная таблица умножения из 81 строки. Поменяйте циклы местами: пусть j станет внешним, а i внутренним — сравните порядок строк с прежним. Затем уберите строку с --- и посмотрите, как сразу теряется граница между тройками.

Вывод

Запустите код, чтобы увидеть вывод.

Вложенные циклы: внешний и внутренний#

Тело цикла — обычный блок, и в нём может стоять ещё один for. Внешний цикл берёт очередное значение i и на каждый свой оборот прокручивает внутренний целиком, от старта до финиша. В таблице выше внешний оборот — это множитель, внутренний перебирает три множимых, поэтому строк девять: три оборота внешнего умножить на три оборота внутреннего. Печать через Printf с глаголами %d — из урока про fmt: вывод и форматирование.

Число оборотов вложенных циклов перемножается, и прикинуть произведение стоит до запуска: 9 × 9 = 81 строка — норм, а два цикла по 10 000 — сто миллионов оборотов, и на таком замедляется не только песочница, но и настоящий сервер. Когда цикл в цикле кажется естественным решением, посчитайте произведение на бумажке: возможно, работу стоит перевернуть или раздробить.

Счётчикам давайте внятные имена: i и j привычны, но в задаче про недели и дни week и day читаются без комментариев. Области видимости здесь те же, что у переменных в заголовке if: week живёт во внешнем цикле, day — во внутреннем, перепутать их не выйдет — компилятор не даст. Когда очередей станет слишком много и работу захочется выполнять не по очереди, а одновременно — это уже другая глава: в курсе go-advanced для этого есть горутины.

Одна оговорка наперёд: break внутри вложенного цикла завершает только свой, внутренний цикл. Полный разговор о break, continue и вечных циклах — тема следующего урока.

Шпаргалка: части заголовка for#

Часть заголовкаКогда срабатываетПример
ИнициализацияОдин раз перед первым оборотомi := 1
УсловиеПеред каждым оборотом; false завершает циклi <= 5
Шаг (post)После каждого оборота телаi++ или i += 2
ТелоКаждый оборот, пока условие истинноsum += i
Все части опущеныВечный цикл, выход через breakfor { } — тема следующего урока

For как while: опущенные части заголовка#

Каждая из трёх частей заголовка опускается — на это и делалась ставка, когда в языке оставили один цикл. Уберите шаг: for i := 0; i < 3; { … i++ } — шаг перекочевал в тело, и цикл по-прежнему считает три оборота; песочница такую форму принимает, проверьте на любом примере выше.

Самая ходовая форма — «только условие», она же «пока»: count := 3; for count > 0 { fmt.Println(count); count-- }. Точек с запятой нет вовсе: когда от заголовка остаётся одно условие, for превращается в while из других языков. Так пишут циклы «пока не кончатся попытки», «пока очередь не опустеет», «пока пользователь не введёт стоп» — шаг в них обычно живёт в теле и часто зависит от того, что произошло внутри оборота.

Если опустить всё — for { } — получится вечный цикл: условия нет, значит, и лгать нечему. Выход организуют внутри тела оператором break; в песочнице конструкция работает, а без выхода упирается в страховку, о которой ниже. Одна честная оговорка про песочницу: редкая форма без счётчика, но с ведущей точкой с запятой — for ; cond; post — законна в настоящем Go, и основной движок песочницы её исполняет, а вот резервный упрощённый движок её не парсит, поэтому в уроках мы пишем счётчик с объявлением до цикла — читается так же, а работает везде. А break, continue и осмысленные вечные циклы — следующая остановка курса: разберём, как выходить из for { } красиво и где бесконечный цикл вообще уместен.

Красивый приём: два счётчика навстречу

GO
package main

import "fmt"

func main() {
        for i, j := 0, 9; i < j; i, j = i+1, j-1 {
                fmt.Println(i, j)
        }
}

Этот код запускается в песочнице — прогоните. В заголовке можно объявить сразу две переменные: i, j := 0, 9, а шаг обновляет обе — i, j = i+1, j-1. Счётчики идут навстречу друг другу, и цикл заканчивается, когда i догоняет j: пары 0–9, 1–8, 2–7, 3–6, 4–5. Такой приём любят в задачах про палиндромы и поиск пары чисел с заданной суммой. Заметьте: обе переменные видны только внутри цикла — после него не существует ни i, ни j.

Соберите программу: посчитаем итог

Соберите программу, которая накапливает сумму чисел от 1 до 4: аккумулятор объявляется до цикла, наращивается в теле, печать идёт после цикла. Правильная программа выводит «итого: 10».

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: цикл for и его заголовок

Термин

Сколько циклов в Go и зачем один?

нажмите, чтобы перевернуть
Определение

Один — for. Заголовок из трёх частей (счётчик; условие; шаг) покрывает классический счёт, а опущением частей получаются «пока»-циклы и вечный цикл. Один оператор — меньше синтаксиса и разночтений.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • В Go один цикл — for; классическая форма for i := 1; i <= 5; i++ { … } собирает счётчик, условие и шаг в одном заголовке.
  • Инициализация выполняется один раз перед первым оборотом, условие проверяется перед каждым оборотом, шаг срабатывает после тела.
  • for i := 0; i < n; i++ делает ровно n оборотов: счётчик пробегает от 0 до n-1; off-by-one ловится проверкой первого и последнего значения счётчика.
  • Счётчик из заголовка виден только внутри цикла; аккумулятор объявляют до цикла — объявленный в теле, он обнуляется каждый оборот.
  • Шаг — любой оператор присваивания: i++, i--, i += 2; главное, чтобы шаг приближал счётчик к границе, иначе цикл не закончится.
  • Вложенные циклы перемножают обороты: на каждый оборот внешнего внутренний прокручивается целиком.
  • Части заголовка опускаются: for cond { … } — «пока», for { } — вечный цикл с выходом через break; break и continue подробно разбираются в следующем уроке.

Домашнее задание

Соберите план тренировок: внешний цикл for week := 1; week <= 4; week++ печатает «неделя N», внутри вложенный for day := 1; day <= 3; day++ печатает «день M: тренировка». Проверьте, что строк ровно двенадцать. Затем добавьте аккумулятор minutes := 0 до внешнего цикла и наращивайте его на 45 в теле внутреннего; после всех циклов напечатайте «итого минут: 540». Наконец, допишите третий цикл for stretch := 2; stretch <= 10; stretch += 2 с печатью «растяжка N» — убедитесь, что номера вышли только чётные.

Личные заметкиПусто
Хранятся только в вашем браузере (Go Base · Цикл for: классическая форма)

Вопросы и ответы по уроку

Почему в Go нет while и do-while?

Заголовок for покрывает эти формы опущением частей: for cond { … } — это и есть while, а do-while собирается из for { … } с проверкой условия в конце тела и выходом через break. Один оператор вместо трёх: меньше синтаксиса, и слово while в Go-коде не встретится в принципе.

Как посчитать обороты, если и старт, и граница — переменные?

Подставьте крайние значения: счётчик начинает со start и работает, пока условие истинно. Для for i := start; i < end; i++ это end - start оборотов, для i <= end — end - start + 1. Сомневаетесь — напечатайте счётчик в первом и последнем обороте и сверьте границы: подстановка краёв ловит почти все ошибки на единицу.

Что будет, если в пост-части написать i + 1 вместо i++?

Пост-часть — это оператор, а не выражение: i++ и i += 1 меняют переменную, а i + 1 лишь вычисляет значение и никуда его не кладёт. Настоящий компилятор отклонит такой заголовок, а песочница пропустит: цикл станет вечным, печать — бесконечной, и её оборвёт лимит строк вывода. Рабочие шаги: i++, i += n или присваивание i = i + 1.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Go Pro25 уроков~14 чпрактика25 уроков уже доступны

Продвинутые возможности Go: от замыканий до рефлексии

Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.

  • Писать и читать конкурентный код: go-функции, каналы, select
  • Управлять жизненным циклом операций через context
  • Проектировать API на интерфейсах
  • Использовать дженерики без злоупотребления