Содержание урока · 10 разделов
- Циклы в golang: почему цикл один
- Цикл for go: счётчик, условие и шаг
- Off-by-one: сколько оборотов сделает цикл
- Накопление: аккумулятор за пределами цикла
- Вложенные циклы: внешний и внутренний
- Шпаргалка: части заголовка for
- For как while: опущенные части заголовка
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Циклы в 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 += "го " собирает строку из кусочков — так накапливают логи, разметку, строки отчётов. Механика прежняя: объявить до цикла, наращивать в теле, использовать после.
Вставьте части заголовка
Соберите заголовок классического цикла: счётчик идёт от единицы до трёх включительно и после каждого оборота растёт на единицу. Готовый цикл должен трижды напечатать строку «итерация» с номером.
Песочница: таблица умножения
Запустите: на каждый оборот внешнего цикла внутренний успевает прокрутиться целиком, поэтому строки идут тройками. Расширьте оба цикла до девяти — получится полная таблица умножения из 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 |
| Все части опущены | Вечный цикл, выход через break | for { } — тема следующего урока |
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 { } красиво и где бесконечный цикл вообще уместен.
Красивый приём: два счётчика навстречу
GOpackage 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.
Вечный цикл чаще всего делают три опечатки: забыт шаг (счётчик не меняется), шаг направлен не туда (i-- при условии i < n) и условие, мимо которого шаг проскакивает: for i := 0; i != 10; i += 3 печатает 0, 3, 6, 9, 12, 15 и никогда не останавливается — десятка пропущена. На настоящем компьютере программа зависнет и будет греть процессор, пока её не убьют. В песочнице на этот случай страховка: программу прервёт лимит — времени, шагов или объёма вывода, смотря что первым исчерпается (например, «превышен лимит времени») — можете убедиться, вставив тот же заголовок в любой runner выше. Правило на каждый день: шаг обязан приближать счётчик к границе, а != для счёта с крупным шагом почти всегда ошибка — пишите < или <=.
Соберите программу: посчитаем итог
Соберите программу, которая накапливает сумму чисел от 1 до 4: аккумулятор объявляется до цикла, наращивается в теле, печать идёт после цикла. Правильная программа выводит «итого: 10».
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: цикл for и его заголовок
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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 нет 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: от замыканий до рефлексии
Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Писать и читать конкурентный код: go-функции, каналы, select
- Управлять жизненным циклом операций через context
- Проектировать API на интерфейсах
- Использовать дженерики без злоупотребления