Iteradores e Geradores em Go (Golang): Um Guia Detalhado
Что это такое и зачем? (аналогия из реальной жизни)
Представь, что ты — библиотекарь, а перед тобой стоит огромный стеллаж с книгами. Твоя задача — прочитать названия всех книг по порядку. У тебя есть два способа это сделать:
Способ 1 (обычный): Ты берёшь сразу все книги со стеллажа, складываешь их на свой стол огромной горой, и только потом начинаешь читать названия. Если книг 10 — это нормально. А если их 10 миллионов? Твой стол просто сломается, а в комнате не останется места. Ты потратишь кучу времени и памяти только на то, чтобы собрать все книги вместе.
Способ 2 (как итератор): Ты подходишь к стеллажу, читаешь название первой книги, идёшь ко второй, читаешь её, потом к третьей... Ты не тащишь все книги сразу. Ты двигаешься по полкам шаг за шагом. В любой момент ты знаешь, на какой книге остановился, и можешь продолжить с этого места.
В программировании итератор — это и есть такой "умный библиотекарь". Он позволяет пройтись по элементам коллекции (списку, массиву, строке) по одному, не загружая всю коллекцию в память целиком.
Генератор — это ещё более крутая штука. Представь, что ты не просто читаешь книги, а сам их пишешь на ходу. Ты можешь придумать бесконечную историю, но рассказывать её будешь по одной главе, только когда тебя просят. Генератор — это функция, которая умеет "замораживать" своё выполнение, возвращать промежуточный результат, а потом "просыпаться" и продолжать с того же места.
💡 Главная идея: Итераторы и генераторы экономят память и время. Они не создают гигантские списки в памяти, а выдают элементы по требованию. Это называется "ленивые вычисления" (lazy evaluation).
Теория: как это работает изнутри
Итераторы в Go: концепция
В Go нет встроенного ключевого слова iterator или yield, как в Python или JavaScript. Но это не значит, что итераторов в Go нет. Они просто реализованы иначе — через интерфейсы и функции обратного вызова (callbacks).
Начиная с версии Go 1.23 (август 2024), в языке появилась официальная поддержка итераторов через пакет iter. Но даже до этого программисты на Go успешно использовали итераторы, просто создавая свои собственные.
Как работает итератор изнутри:
- У тебя есть коллекция данных (например, срез строк).
- Ты создаёшь функцию-итератор, которая принимает другую функцию (коллбэк).
- Итератор проходит по элементам и для каждого вызывает твой коллбэк.
- Если коллбэк возвращает
false, итератор останавливается.
Это называется pull-итератор (вытягивающий) — ты "вытягиваешь" данные из источника по одному элементу.
Генераторы в Go: концепция
Генератор — это функция, которая возвращает итератор. В Go генераторы обычно реализуются через замыкания (closures) или через каналы (channels).
Как работает генератор через замыкание:
- Функция-генератор создаёт локальную переменную (состояние).
- Она возвращает другую функцию (замыкание), которая "помнит" это состояние.
- При каждом вызове возвращённой функции она обновляет состояние и возвращает следующий элемент.
Как работает генератор через канал:
- Функция-генератор создаёт канал.
- Запускает горутину (лёгкий поток), которая отправляет элементы в канал.
- Возвращает канал, из которого можно читать элементы по одному.
⚠️ Важно: В Go нет встроенного синтаксисаyield. Генераторы здесь — это паттерн проектирования, а не языковая конструкция. Но с версии Go 1.23 появился пакетiter, который делает работу с итераторами более удобной.
Синтаксис в Go (Golang): все формы и варианты
1. Классический итератор через коллбэк (до Go 1.23)
Это самый распространённый способ. Функция принимает другую функцию и вызывает её для каждого элемента.
package main
import "fmt"
// Итератор для среза чисел
// Принимает срез и функцию-обработчик
// Если обработчик вернул false, итерация прекращается
func IterateSlice(numbers []int, callback func(int) bool) {
for _, num := range numbers {
// Вызываем callback для каждого числа
// Если callback вернул false — останавливаемся
if !callback(num) {
break
}
}
}
func main() {
data := []int{10, 20, 30, 40, 50}
fmt.Println("Итерация по срезу:")
IterateSlice(data, func(n int) bool {
fmt.Println("Элемент:", n)
// Всегда возвращаем true, чтобы продолжить
return true
})
fmt.Println("
Итерация с остановкой:")
IterateSlice(data, func(n int) bool {
fmt.Println("Элемент:", n)
if n > 30 {
fmt.Println("Стоп! Достигли предела.")
return false // Останавливаем итерацию
}
return true
})
}
2. Итератор с возвратом значения (pull-итератор)
Здесь итератор — это функция, которая при каждом вызове возвращает следующий элемент и признак окончания.
package main
import "fmt"
// Функция, возвращающая итератор для среза
// Возвращает: функцию-итератор и функцию для сброса
func CreateSliceIterator(slice []string) (next func() (string, bool), reset func()) {
index := 0 // Текущая позиция (замыкание)
// next — это замыкание, которое "помнит" index
next = func() (string, bool) {
if index >= len(slice) {
return "", false // Больше элементов нет
}
value := slice[index]
index++ // Сдвигаем указатель
return value, true // Возвращаем элемент и true
}
// reset — сбрасывает итератор в начало
reset = func() {
index = 0
}
return next, reset
}
func main() {
fruits := []string{"Яблоко", "Банан", "Апельсин", "Груша"}
// Получаем итератор
next, reset := CreateSliceIterator(fruits)
fmt.Println("Первая итерация:")
for {
fruit, ok := next()
if !ok {
break // Выходим, если элементов больше нет
}
fmt.Println("Фрукт:", fruit)
}
// Сбрасываем итератор
reset()
fmt.Println("
Вторая итерация (после сброса):")
for {
fruit, ok := next()
if !ok {
break
}
fmt.Println("Фрукт:", fruit)
}
}
3. Итератор через каналы (горутины)
Это самый "Go-way" способ. Каналы идеально подходят для передачи последовательностей данных.
package main
import "fmt"
// Генератор чисел Фибоначчи через канал
// Возвращает канал, в который будут отправляться числа
func FibonacciGenerator(limit int) <-chan int {
ch := make(chan int) // Создаём небуферизированный канал
// Запускаем горутину-генератор
go func() {
// Закрываем канал после завершения генерации
defer close(ch)
a, b := 0, 1
for i := 0; i < limit; i++ {
ch <- a // Отправляем число в канал
a, b = b, a+b // Вычисляем следующее
}
}()
return ch // Возвращаем канал для чтения
}
func main() {
fmt.Println("Числа Фибоначчи (первые 10):")
// Используем range для чтения из канала
for num := range FibonacciGenerator(10) {
fmt.Printf("%d ", num)
}
fmt.Println()
}
4. Итератор с использованием пакета iter (Go 1.23+)
Начиная с Go 1.23, в стандартной библиотеке появился пакет iter, который вводит два основных типа:
iter.Seq[V]— последовательность значенийiter.Seq2[K, V]— последовательность пар (ключ, значение)
// Внимание: этот код требует Go 1.23 или новее
package main
import (
"fmt"
"iter"
)
// Функция, возвращающая итератор (iter.Seq)
func EvenNumbers(limit int) iter.Seq[int] {
// Возвращаем функцию, которая принимает yield
return func(yield func(int) bool) {
for i := 0; i <= limit; i += 2 {
// yield возвращает false, если нужно остановиться
if !yield(i) {
return
}
}
}
}
func main() {
fmt.Println("Чётные числа до 10:")
// Используем for range с итератором
for num := range EvenNumbers(10) {
fmt.Printf("%d ", num)
}
fmt.Println()
// Итератор с парой (индекс, значение)
words := []string{"Go", "Python", "Rust"}
fmt.Println("
Срез с индексами:")
for idx, word := range SliceIterator(words) {
fmt.Printf("[%d] %s
", idx, word)
}
}
// Функция, возвращающая итератор с парами
func SliceIterator[T any](slice []T) iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range slice {
if !yield(i, v) {
return
}
}
}
}
5. Бесконечный генератор
Генераторы могут быть бесконечными — они никогда не заканчиваются, но занимают фиксированное количество памяти.
package main
import (
"fmt"
"math/rand"
)
// Бесконечный генератор случайных чисел
// Возвращает функцию, которая каждый раз даёт новое число
func InfiniteRandom() func() int {
return func() int {
return rand.Intn(100) // Случайное число от 0 до 99
}
}
func main() {
nextRandom := InfiniteRandom()
fmt.Println("10 случайных чисел:")
for i := 0; i < 10; i++ {
fmt.Printf("%d ", nextRandom())
}
fmt.Println()
// Бесконечный генератор через канал
fmt.Println("
Ещё 5 случайных (через канал):")
ch := InfiniteRandomChannel()
for i := 0; i < 5; i++ {
fmt.Printf("%d ", <-ch)
}
fmt.Println()
}
// Бесконечный генератор через канал
func InfiniteRandomChannel() <-chan int {
ch := make(chan int)
go func() {
for {
ch <- rand.Intn(100)
}
}()
return ch
}
6. Комбинирование итераторов
Итераторы можно комбинировать: фильтровать, преобразовывать, объединять.
package main
import "fmt"
// Функция, которая фильтрует элементы
func FilterIterator[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}
// Функция, которая преобразует элементы (map)
func MapIterator[T, U any](slice []T, transform func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = transform(v)
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// Цепочка: фильтр -> преобразование
evenNumbers := FilterIterator(numbers, func(n int) bool {
return n%2 == 0 // Оставляем только чётные
})
squaredNumbers := MapIterator(evenNumbers, func(n int) int {
return n * n // Возводим в квадрат
})
fmt.Println("Исходные числа:", numbers)
fmt.Println("Чётные числа:", evenNumbers)
fmt.Println("Квадраты чётных:", squaredNumbers)
// Работа со строками
names := []string{"анна", "иван", "мария", "петр"}
capitalized := MapIterator(names, func(s string) string {
if len(s) == 0 {
return s
}
// Первую букву в верхний регистр
runes := []rune(s)
runes[0] = rune(runes[0] - 32) // 'а' -> 'А'
return string(runes)
})
fmt.Println("
Имена:", names)
fmt.Println("С заглавной:", capitalized)
}
Примеры кода — от простого к сложному
Пример 1: Простейший итератор для чисел
package main
import "fmt"
// Простейший итератор: просто выводит числа от 1 до N
func SimpleIterator(n int) {
for i := 1; i <= n; i++ {
fmt.Println("Число:", i)
}
}
func main() {
fmt.Println("=== Пример 1: Простейший итератор ===")
SimpleIterator(5)
}
Пример 2: Итератор с остановкой по условию
package main
import "fmt"
// Итератор, который можно остановить извне
func ConditionalIterator(start, end int, stopWhen func(int) bool) {
for i := start; i <= end; i++ {
if stopWhen(i) {
fmt.Printf("Остановка на числе %d
", i)
break
}
fmt.Println("Проход:", i)
}
}
func main() {
fmt.Println("=== Пример 2: Итератор с остановкой ===")
// Останавливаемся, когда число больше 7
ConditionalIterator(1, 15, func(n int) bool {
return n > 7
})
}
Пример 3: Генератор чётных чисел
package main
import "fmt"
// Генератор чётных чисел (через замыкание)
func EvenGenerator() func() int {
current := 0 // Начальное значение
return func() int {
result := current
current += 2 // Увеличиваем на 2 для следующего вызова
return result
}
}
func main() {
fmt.Println("=== Пример 3: Генератор чётных чисел ===")
nextEven := EvenGenerator()
fmt.Println("Первые 6 чётных чисел:")
for i := 0; i < 6; i++ {
fmt.Printf("%d ", nextEven())
}
fmt.Println()
}
Пример 4: Итератор для строки (по символам)
package main
import "fmt"
// Итератор для строки — проходит по рунам (символам)
func StringRuneIterator(s string) func() (rune, bool) {
runes := []rune(s) // Преобразуем строку в срез рун
index := 0
return func() (rune, bool) {
if index >= len(runes) {
return 0, false // Строка закончилась
}
r := runes[index]
index++
return r, true
}
}
func main() {
fmt.Println("=== Пример 4: Итератор для строки ===")
text := "Привет, Go!"
nextRune := StringRuneIterator(text)
fmt.Printf("Строка: %s
", text)
fmt.Println("Символы по одному:")
for {
r, ok := nextRune()
if !ok {
break
}
fmt.Printf(" Символ: '%c' (код: %d)
", r, r)
}
}
Пример 5: Генератор простых чисел
package main
import "fmt"
// Генератор простых чисел (через канал)
func PrimeGenerator(limit int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for num := 2; num <= limit; num++ {
isPrime := true
// Проверяем, делится ли число на что-то кроме 1 и себя
for divisor := 2; divisor*divisor <= num; divisor++ {
if num%divisor == 0 {
isPrime = false
break
}
}
if isPrime {
ch <- num // Отправляем простое число в канал
}
}
}()
return ch
}
func main() {
fmt.Println("=== Пример 5: Генератор простых чисел ===")
fmt.Println("Простые числа до 30:")
for prime := range PrimeGenerator(30) {
fmt.Printf("%d ", prime)
}
fmt.Println()
}
Пример 6: Итератор для файла (построчное чтение)
package main
import (
"bufio"
"fmt"
"strings"
)
// Итератор для чтения строк из текста
func LineIterator(text string) func() (string, bool) {
scanner := bufio.NewScanner(strings.NewReader(text))
// scanner.Scan() возвращает true, пока есть строки
return func() (string, bool) {
if scanner.Scan() {
return scanner.Text(), true
}
return "", false
}
}
func main() {
fmt.Println("=== Пример 6: Итератор для строк текста ===")
poem := `Вот моя деревня,
Вот мой дом родной,
Вот качусь я в санках
По горе крутой.`
nextLine := LineIterator(poem)
fmt.Println("Строки стихотворения:")
lineNum := 1
for {
line, ok := nextLine()
if !ok {
break
}
fmt.Printf(" %d: %s
", lineNum, line)
lineNum++
}
}
Пример 7: Комбинированный итератор с фильтрацией и трансформацией
package main
import "fmt"
// Универсальный итератор с цепочкой операций
type Pipeline struct {
data []int
}
// Создаёт конвейер из среза
func NewPipeline(data []int) *Pipeline {
return &Pipeline{data: data}
}
// Фильтр: оставляет только те элементы, которые проходят проверку
func (p *Pipeline) Filter(predicate func(int) bool) *Pipeline {
var filtered []int
for _, v := range p.data {
if predicate(v) {
filtered = append(filtered, v)
}
}
p.data = filtered
return p
}
// Трансформация: применяет функцию к каждому элементу
func (p *Pipeline) Map(transform func(int) int) *Pipeline {
for i, v := range p.data {
p.data[i] = transform(v)
}
return p
}
// Собирает результат в срез
func (p *Pipeline) Collect() []int {
result := make([]int, len(p.data))
copy(result, p.data)
return result
}
func main() {
fmt.Println("=== Пример 7: Конвейер (Pipeline) ===")
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
result := NewPipeline(numbers).
Filter(func(n int) bool { return n%2 == 0 }). // Оставляем чётные
Filter(func(n int) bool { return n > 4 }). // Оставляем > 4
Map(func(n int) int { return n * 10 }). // Умножаем на 10
Collect() // Собираем результат
fmt.Println("Исходные числа:", numbers)
fmt.Println("После конвейера:", result)
}
Сравнение с Python: чем похоже и чем отличается
| Характеристика | Python | Go (Golang) |
|---|---|---|
| Ключевые слова | yield, next(), iter() |
Нет встроенных. Используются функции, замыкания, каналы |
| Стандартная библиотека | itertools (мощный набор инструментов) |
iter (с Go 1.23, базовый функционал) |
| Ленивые вычисления | Да, по умолчанию (range, map, filter — ленивые) | Да, через каналы и замыкания |
| Бесконечные итераторы | Да (itertools.count(), itertools.cycle()) |
Да (через каналы или замыкания) |
| Производительность | Ниже (из-за накладных расходов на yield) | Выше (компилируемый язык, минимум абстракций) |
| Параллелизм | Сложный (GIL, threading) | Встроенный (горутины + каналы — идеально для генераторов) |
| Типизация | Динамическая (ошибки на этапе выполнения) | Статическая (ошибки на этапе компиляции) |
| Синтаксис генератора | def gen(): yield x |
func gen() <-chan T { ... } или замыкание |
| Использование в цикле | for x in gen(): |
for x := range gen() {} (с каналами) |
Пример сравнения: генератор квадратов чисел
// Go: генератор квадратов через канал
func SquareGeneratorGo(limit int) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 1; i <= limit; i++ {
ch <- i * i
}
}()
return ch
}
// Использование:
for sq := range SquareGeneratorGo(5) {
fmt.Println(sq)
}
# Python: генератор квадратов
def square_generator_py(limit):
for i in range(1, limit + 1):
yield i * i
# Использование:
for sq in square_generator_py(5):
print(sq)
💡 Ключевое отличие: В Python генераторы — это часть языка (ключевое слово yield). В Go генераторы — это паттерн, который вы строите сами. Go даёт вам кирпичи (каналы, горутины, замыкания), а Python даёт готовый дом. Но в Go у вас больше контроля и производительности.
Типичные ошибки и подводные камни
Ошибка 1: Забыли закрыть канал
// ❌ НЕПРАВИЛЬНО: канал не закрывается
func BadGenerator() <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch <- i
}
// Забыли close(ch) — горутина зависнет!
}()
return ch
}
// ✅ ПРАВИЛЬНО: всегда закрываем канал
func GoodGenerator() <-chan int {
ch := make(chan int)
go func() {
defer close(ch) // Закроется при выходе из горутины
for i := 0; i < 5; i++ {
ch <- i
}
}()
return ch
}
Ошибка 2: Гонка данных (data race) в замыкании
// ❌ НЕПРАВИЛЬНО: гонка данных
func BadClosureGenerator() func() int {
x := 0
return func() int {
x++ // Небезопасно при параллельном доступе
return x
}
}
// ✅ ПРАВИЛЬНО: используем мьютекс или канал
func SafeClosureGenerator() func() int {
var mu sync.Mutex
x := 0
return func() int {
mu.Lock()
defer mu.Unlock()
x++
return x
}
}
Ошибка 3: Утечка горутин
// ❌ НЕПРАВИЛЬНО: горутина останется висеть
func LeakyGenerator() <-chan int {
ch := make(chan int)
go func() {
for i := 0; ; i++ { // Бесконечный цикл!
ch <- i
}
}()
return ch
}
// Использование:
ch := LeakyGenerator()
fmt.Println(<-ch) // Прочитали одно значение
// Горутина продолжает работать в фоне — утечка!
// ✅ ПРАВИЛЬНО: используем контекст для отмены
func SafeGenerator(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; ; i++ {
select {
case ch <- i:
case <-ctx.Done(): // Отменяем по сигналу
return
}
}
}()
return ch
}
Ошибка 4: Изменение коллекции во время итерации
// ❌ НЕПРАВИЛЬНО: изменение среза во время итерации
slice := []int{1, 2, 3, 4, 5}
for i, v := range slice {
if v == 3 {
slice = append(slice, 6) // Меняем срез!
}
fmt.Println(i, v)
}
// ✅ ПРАВИЛЬНО: создаём копию или используем индексы
slice = []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
if slice[i] == 3 {
slice = append(slice, 6)
}
fmt.Println(i, slice[i])
}
Ошибка 5: Путаница между iter.Seq и iter.Seq2
// ❌ НЕПРАВИЛЬНО: путаем типы итераторов
func WrongIterator() iter.Seq[int] {
return func(yield func(int) bool) {
// Правильно
}
}
// Использование:
for k, v := range WrongIterator() { // Ошибка! Seq не даёт пары
// ...
}
// ✅ ПРАВИЛЬНО: используем правильный тип
func RightIterator() iter.Seq2[int, string] {
return func(yield func(int, string) bool) {
yield(1, "один")
yield(2, "два")
}
}
for k, v := range RightIterator() {
fmt.Println(k, v)
}
Лучшие практики и советы профессионалов
1. Выбирайте правильный тип итератора
| Ситуация | Рекомендуемый подход |
|---|---|
| Нужно просто пройтись по коллекции | Используйте встроенный for range |
| Нужна ленивая генерация данных | Каналы (channels) — самый Go-way способ |
| Нужно сохранить состояние между вызовами | Замыкания (closures) |
| Нужна совместимость с Go 1.23+ | Пакет iter |
| Нужна максимальная производительность | Обычный цикл for с индексами |
2. Всегда закрывайте каналы
Используйте defer close(ch) сразу после создания канала в горутине. Это гарантирует, что канал закроется, даже если произойдёт паника.
3. Используйте контекст для отмены
Если ваш генератор может работать долго или бесконечно, передавайте ему context.Context для graceful shutdown.
func GeneratorWithContext(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; ; i++ {
select {
case ch <- i:
case <-ctx.Done():
return // Завершаем горутину
}
}
}()
return ch
}
4. Не злоупотребляйте итераторами для простых задач
Если у вас небольшой срез (до