Циклы в Go: разбор на примере с for, range и подводными камнями
В Go всего одна конструкция цикла — for. Разбираем её формы на рабочих примерах: классический счётчик, итерация по коллекциям через range, бесконечный цикл, break с метками и ловушки, на которых спотыкаются даже опытные разработчики.

Одна конструкция вместо трёх
В большинстве языков есть for, while и do-while. Go обошёлся одним ключевым словом for, которое покрывает все три случая. Это упрощает грамматику, но требует понимания нескольких форм записи — и нюансов, которые компилятор не подскажет.
Ниже — рабочие примеры для Go 1.22+. Каждый можно скопировать в файл и запустить через go run. Если вы используете версию до 1.22, обратите внимание на раздел о переменной цикла — поведение range там другое.
Классический for с тремя частями
Самая привычная форма: инициализация, условие, пост-выражение.
package main
import "fmt"
func main() {
// Печатаем числа от 0 до 4
for i := 0; i < 5; i++ {
fmt.Println(i)
}
}
Любую из трёх частей можно опустить. Если убрать все три — получится бесконечный цикл:
for {
// работает, пока не встретит break или return
}
Если оставить только условие — получится аналог while:
n := 1
for n < 1000 {
n *= 2
}
fmt.Println(n) // 1024
Итерация через range
range — второй (и самый частый на практике) способ использовать for. Он работает со слайсами, массивами, картами, строками и каналами.
Слайс
colors := []string{"red", "green", "blue"}
for index, value := range colors {
fmt.Printf("index=%d value=%s\n", index, value)
}
Если индекс не нужен, его отбрасывают через _:
for _, value := range colors {
fmt.Println(value)
}
Карта (map)
population := map[string]int{
"Moscow": 13_100_000,
"Berlin": 3_700_000,
"Tokyo": 14_000_000,
}
for city, pop := range population {
fmt.Printf("%s: %d\n", city, pop)
}
Порядок обхода карты не гарантирован — при каждом запуске он может отличаться. Это сделано намеренно, чтобы код не зависел от порядка вставки.
Строка
for i, r := range "Привет" {
fmt.Printf("byte offset=%d rune=%c\n", i, r)
}
range по строке итерирует по рунам (Unicode code points), а не по байтам. Переменная i — это смещение в байтах, а r — руна. Для кириллицы одна руна занимает два байта, поэтому i будет прыгать через два.
Сноска: Руна (rune) в Go — это псевдоним типа int32, представляющий один Unicode code point. Один символ кириллицы или эмодзи может занимать от 2 до 4 байт в UTF-8, но в range по строке Go декодирует их автоматически.
Канал
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
// Цикл завершится, когда канал закрыт и пуст
for val := range ch {
fmt.Println(val)
}
Сравнение форм цикла
| Форма | Когда использовать | Что возвращает |
|---|---|---|
for init; cond; post {} | Счётчик с известным шагом | Ничего — переменные объявляете сами |
for cond {} | Цикл с условием (аналог while) | Ничего |
for {} | Бесконечный цикл, выход через break/return | Ничего |
for i, v := range slice | Обход слайса / массива | Индекс + значение |
for k, v := range map | Обход карты | Ключ + значение |
for i, r := range string | Обход строки по рунам | Байтовое смещение + руна |
for v := range channel | Чтение из канала до закрытия | Значение |
break и continue с метками
В Go break по умолчанию прерывает ближайший цикл. При вложенных циклах это создаёт проблему — вам нужно выйти из внешнего, а выходите только из внутреннего. Решение — метки (labels).
package main
import "fmt"
func main() {
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
var target int
outer:
for row := range matrix {
for col := range matrix[row] {
if matrix[row][col] == 5 {
target = matrix[row][col]
break outer // выходим из обоих циклов
}
}
}
fmt.Println("found:", target) // found: 5
}
continue с меткой работает аналогично — переходит к следующей итерации указанного внешнего цикла.
Ловушка: переменная цикла до Go 1.22
В Go до версии 1.22 переменная итерации в for ... range переиспользовалась на каждой итерации. Если вы захватывали её в горутину или замыкание, все горутины видели последнее значение, а не своё. Это был источник одного из самых массовых багов в Go-коде.
Пример проблемного кода (Go 1.21 и ниже):
values := []string{"a", "b", "c"}
for _, v := range values {
go func() {
fmt.Println(v) // все горутины могли напечатать "c"
}()
}
Классический обходной путь — передать переменную аргументом:
for _, v := range values {
go func(val string) {
fmt.Println(val)
}(v)
}
Начиная с Go 1.22, каждая итерация создаёт новую переменную. Проблема ушла на уровне языка. Но если ваш проект собирается под Go 1.21 или ниже — ловушка всё ещё актуальна. Проверьте директиву go в go.mod: именно она определяет семантику цикла.
range по целому числу (Go 1.22+)
В Go 1.22 появилась новая форма — range по целому числу:
for i := range 5 {
fmt.Println(i) // 0, 1, 2, 3, 4
}
Это синтаксический сахар вместо for i := 0; i < 5; i++. Выглядит мелочью, но убирает целый класс ошибок «на единицу» (off-by-one), потому что верхняя граница всегда исключена, как в range(n) в Python.
Типичные ошибки
- Модификация слайса во время итерации. Добавление элементов через
appendвнутриfor rangeне приведёт к бесконечному циклу — длина фиксируется при входе в цикл. Но удаление или вставка элементов по индексу может дать неожиданные пропуски. Если нужно фильтровать слайс, безопаснее собирать результат в новый. - Итерация по карте и удаление ключей. Спецификация Go разрешает удалять ключи из карты внутри
range. Но добавлять ключи — непредсказуемо: новый ключ может попасть или не попасть в итерацию. - range по строке с невалидным UTF-8. Если строка содержит битые байты,
rangeподставит рунуU+FFFD(replacement character). Программа не упадёт, но данные могут молча исказиться. - Забытый
closeдля канала.for range chбудет ждать вечно, если канал не закрыт — получите утечку горутины.
Что дальше
Циклы — механизм элементарный, но в Go именно вокруг них концентрируются неочевидные баги: захват переменной в горутине, мутация коллекции во время обхода, некорректный обход строк побайтово вместо range. Следующий шаг — пройтись по своему коду и проверить: все ли замыкания внутри циклов безопасны, нет ли побайтовых обходов строк там, где ожидаются руны. Инструмент go vet ловит часть таких проблем — запускайте его в CI, если ещё не делаете.
Разбор научной статьи: Сложный текст — понятным языком, с терминами
Спросить за 12 ₽Источники: The Go Programming Language Specification — For statements, Go Wiki: LoopvarExperiment (изменение семантики переменной цикла в Go 1.22)
Частые вопросы
Почему в Go нет while и do-while?
Как прервать вложенный цикл, не используя метку?
Что будет, если я изменю слайс внутри for range?
Код с горутиной внутри цикла работает некорректно — все горутины видят одно значение. Что делать?
Можно ли использовать range по целому числу в Go 1.21?
Как итерировать по строке побайтово, а не по рунам?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.