Цикл for: все варианты + типичные ошибки

В Go философия простоты доведена до абсолюта - есть только один цикл for. Но не спешите расстраиваться! Он умеет всё, что умеют while, do-while и foreach в других языках.

Классический for

Самый привычный вариант с тремя частями:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}
Переменная i видна только внутри цикла. После цикла она не существует.

For как while

Можно использовать только условие:

count := 0
for count < 5 {
    fmt.Println(count)
    count++
}

// Или с более сложным условием
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
    line := scanner.Text()
    if line == "exit" {
        break
    }
}

Бесконечный цикл

Просто for без условий:

for {
    // Бесконечный цикл
    // Выход через break или return

    if someCondition {
        break
    }
}

Range - итерация по коллекциям

Слайсы и массивы

fruits := []string{"яблоко", "банан", "апельсин"}

// Индекс и значение
for i, fruit := range fruits {
    fmt.Printf("%d: %s\n", i, fruit)
}

// Только значение (индекс игнорируем через _)
for _, fruit := range fruits {
    fmt.Println(fruit)
}

// Только индекс
for i := range fruits {
    fmt.Println(i)
}

Строки (по рунам, не байтам!)

word := "Привет"
for i, char := range word {
    fmt.Printf("Позиция %d: %c\n", i, char)
}
// Выведет правильно каждую букву, даже кириллицу
Range по строке итерирует по рунам (Unicode символам), а не по байтам. Это правильно обрабатывает многобайтовые символы.

Maps

ages := map[string]int{
    "Алиса": 30,
    "Боб":   25,
}

for name, age := range ages {
    fmt.Printf("%s: %d лет\n", name, age)
}

Каналы

ch := make(chan int)

// В другой горутине
go func() {
    ch <- 1
    ch <- 2
    close(ch)
}()

// Читаем из канала до его закрытия
for num := range ch {
    fmt.Println(num)
}

Break и Continue

Break - выход из цикла

for i := 0; i < 10; i++ {
    if i == 5 {
        break // Выходим при i = 5
    }
    fmt.Println(i)
}
// Выведет: 0 1 2 3 4

Continue - переход к следующей итерации

for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue // Пропускаем четные
    }
    fmt.Println(i)
}
// Выведет: 1 3 5 7 9

Метки (labels)

Для выхода из вложенных циклов:

outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer // Выход из обоих циклов
        }
        fmt.Printf("(%d,%d) ", i, j)
    }
}

Практические примеры

Поиск в слайсе

func contains(slice []string, target string) bool {
    for _, item := range slice {
        if item == target {
            return true
        }
    }
    return false
}

Фильтрация

numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
var evens []int

for _, n := range numbers {
    if n%2 == 0 {
        evens = append(evens, n)
    }
}
// evens: [2 4 6 8 10]

Retry с ограничением попыток

maxRetries := 3
for attempt := 1; attempt <= maxRetries; attempt++ {
    if err := doSomething(); err == nil {
        break // Успех!
    }

    if attempt == maxRetries {
        log.Fatal("Не удалось после", maxRetries, "попыток")
    }

    time.Sleep(time.Second * time.Duration(attempt))
}

Типичные ошибки

<ComparisonTable data={{ headers: ["Ошибка", "Неправильно", "Правильно"], rows: [ ["Изменение слайса в range", "for i, v := range slice {\n slice[i] = v * 2\n}", "for i := range slice {\n slice[i] *= 2\n}"], ["Захват переменной цикла", "for _, v := range vals {\n go func() { fmt.Println(v) }()\n}", "for _, v := range vals {\n v := v // копия\n go func() { fmt.Println(v) }()\n}"], ["Бесконечный цикл с append", "for i := 0; i < len(s); i++ {\n s = append(s, i)\n}", "length := len(s)\nfor i := 0; i < length; i++ {\n // ...\n}"] ] }} />

Производительность

Pre-allocate слайсы

// Плохо
var result []int
for i := 0; i < 1000; i++ {
    result = append(result, i*2)
}

// Хорошо
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
    result = append(result, i*2)
}

Избегайте лишних аллокаций

// Плохо - создаёт копию всего слайса
for _, item := range hugeSlice {
    process(item)
}

// Хорошо для больших структур - используйте индекс
for i := range hugeSlice {
    process(&hugeSlice[i])
}

Полезные паттерны

Sliding window

data := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
windowSize := 3

for i := 0; i <= len(data)-windowSize; i++ {
    window := data[i : i+windowSize]
    fmt.Println(window)
}
// [1 2 3], [2 3 4], [3 4 5], ...

Обратный цикл

slice := []string{"a", "b", "c", "d"}

// Обратная итерация
for i := len(slice) - 1; i >= 0; i-- {
    fmt.Println(slice[i])
}
// d, c, b, a

Итоги

  • В Go только один цикл for, но он универсальный
  • range - мощный инструмент для итерации
  • Помните про область видимости переменных
  • Осторожно с захватом переменных в горутинах
  • Pre-allocate слайсы для производительности

В следующем уроке изучим функции - первоклассные объекты в Go!

Типичная ошибка

Писать for i := 0; i < len(s); i++ там, где достаточно range. range чище, безопаснее и читается с первого взгляда.

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.