Паттерны итераторов Go 1.24: 7 продакшен-паттернов от Range Over Func до конвейеров данных
Когда циклы for встречаются с функциями: смена парадигмы итераторов в Go
На прошлой неделе я рефакторил сервис обработки данных с 3 уровнями вложенных циклов for, обрабатывающих 100K записей — память подскочила до 2 ГБ, потому что каждый уровень должен был хранить промежуточные результаты в срезах перед передачей на следующий. После перехода на конвейер итераторов память снизилась до 15 МБ, а обработка стала на 30% быстрее. Ключевой сдвиг: больше никакого «собери, затем обработай» — вместо этого «итерируй и обрабатывай на лету».
Go 1.24 официально стабилизировал синтаксис range over func и пакет iter, предоставив Go нативные итераторы с нулевым выделением памяти и возможностью композиции. Это не синтаксический сахар — это фундаментальный сдвиг в парадигме обработки данных. В этой статье рассматриваются 7 продакшен-паттернов итераторов Go, которые помогут вам создавать эффективные, элегантные, компонуемые конвейеры данных.
Ключевые выводы
- range over func — основной синтаксис итераторов: Go 1.24 позволяет напрямую итерировать по функциям
- Преобразование Push/Pull итераторов: Поймите оба направления, освойте преобразование
iter.Pull - Композиция конвейеров данных: Цепочки Map/Filter/Reduce с нулевым промежуточным выделением памяти
- Ленивые вычисления и бесконечные последовательности: Вычисления по требованию, обработка бесконечных потоков данных
- Параллельные итераторы и Fan-out: Несколько горутин, параллельно потребляющих итераторы
- Обработка ошибок в итераторах: Изящная обработка ошибок во время итерации
- Проектирование продакшен-библиотеки итераторов: Создание переиспользуемых наборов инструментов итераторов
Содержание
- Справочник по основным концепциям итераторов Go
- Паттерн 1: Базовый итератор range over func
- Паттерн 2: Преобразование Push/Pull итераторов
- Паттерн 3: Композиция конвейеров данных (Map/Filter/Reduce)
- Паттерн 4: Ленивые вычисления и бесконечные последовательности
- Паттерн 5: Параллельные итераторы и Fan-out
- Паттерн 6: Обработка ошибок в итераторах
- Паттерн 7: Проектирование продакшен-библиотеки итераторов
- 5 распространённых ошибок и решения
- 10 распространённых ошибок и способы их устранения
- Продвинутые советы по оптимизации
- Сравнительный анализ: Итераторы vs Каналы vs Срезы
- Рекомендуемые онлайн-инструменты
- Итоги
Справочник по основным концепциям итераторов Go
| Концепция | Сигнатура | Назначение | Пример |
|---|---|---|---|
| Функция-итератор | func(yield func(V) bool) |
Итератор с одним значением | func(yield func(int) bool) |
| Итератор ключ-значение | func(yield func(K, V) bool) |
Итерация по парам ключ-значение | func(yield func(int, string) bool) |
| Pull-итератор | func() (V, bool) |
Извлечение по требованию | next, stop := iter.Pull(seq) |
| iter.Pull | func(Seq[V]) (func() (V, bool), func()) |
Преобразование Push в Pull | Обход, управляемый потребителем |
| iter.Stop | Встроенная функция остановки | Раннее завершение | stop() освобождает ресурсы |
| Возвращаемое значение yield | bool |
Управление продолжением/остановкой итерации | yield(v) возвращает false для остановки |
| Ленивые вычисления | Отложенные вычисления | Генерация значений по требованию | Бесконечные последовательности, строки файла |
| Композиция конвейеров | Цепочки функций | Нулевое промежуточное выделение памяти | Filter(Map(Seq, fn), pred) |
Паттерн 1: Базовый итератор range over func
Проблема: Ловушки памяти при традиционном обходе
func GetAllUsers(db *sql.DB) ([]User, error) {
rows, err := db.Query("SELECT id, name, email FROM users")
if err != nil {
return nil, err
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return nil, err
}
users = append(users, u)
}
return users, rows.Err()
}
1 миллион пользователей? 1 миллион структур User загружается в память. Вам нужны только первые 10? Очень жаль — сначала загрузите их все.
Решение: Итератор range over func
package iterator
import (
"database/sql"
"iter"
)
type User struct {
ID int
Name string
Email string
}
func AllUsers(db *sql.DB) iter.Seq2[int, User] {
return func(yield func(int, User) bool) {
rows, err := db.Query("SELECT id, name, email FROM users")
if err != nil {
return
}
defer rows.Close()
i := 0
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
return
}
if !yield(i, u) {
return
}
i++
}
}
}
Использование:
for i, user := range AllUsers(db) {
fmt.Printf("%d: %s\n", i, user.Name)
if i >= 9 {
break
}
}
Когда выполняется break, yield возвращает false, и функция-итератор немедленно завершается — запрошено только 10 строк, соединение с базой данных корректно закрыто.
Поток выполнения итератора
┌─────────────┐ yield(v) ┌──────────────┐
│ Iterator │ ──────────────→ │ range loop │
│ (producer) │ │ (consumer) │
│ │ ←────────────── │ │
│ │ yield returns │ │
│ │ bool │ │
└─────────────┘ └──────────────┘
│ │
│ yield returns false → return │
│ (early termination) │
└──────────────────────────────────┘
Итератор с одним значением vs итератор ключ-значение
type IntSlice []int
func (s IntSlice) Values() iter.Seq[int] {
return func(yield func(int) bool) {
for _, v := range s {
if !yield(v) {
return
}
}
}
}
func (s IntSlice) All() iter.Seq2[int, int] {
return func(yield func(int, int) bool) {
for i, v := range s {
if !yield(i, v) {
return
}
}
}
}
nums := IntSlice{10, 20, 30}
for v := range nums.Values() {
fmt.Println(v)
}
for i, v := range nums.All() {
fmt.Printf("index=%d value=%d\n", i, v)
}
Паттерн 2: Преобразование Push/Pull итераторов
Различия между Push и Pull итераторами
Push Iterator (iter.Seq) Pull Iterator (func() (V, bool))
┌──────────────────┐ ┌──────────────────┐
│ Producer pushes │ │ Consumer pulls │
│ yield(v) → cons. │ │ next() → prod. │
│ │ │ │
│ Good for: range │ │ Good for: manual │
│ Good for: pipes │ │ Good for: peek │
│ Good for: lazy │ │ Good for: interop │
└──────────────────┘ └──────────────────┘
│ │
│ iter.Pull() conversion │
└──────────────────────────────────┘
Использование преобразования iter.Pull
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
next, stop := iter.Pull(Countdown(5))
defer stop()
for {
v, ok := next()
if !ok {
break
}
fmt.Println(v)
if v == 3 {
fmt.Println("Early termination")
break
}
}
}
Практические применения Pull-итератора: Peek и Take
package iterutil
import "iter"
func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
return func(yield func(V) bool) {
count := 0
for v := range seq {
if count >= n {
return
}
if !yield(v) {
return
}
count++
}
}
}
func First[V any](seq iter.Seq[V]) (V, bool) {
next, stop := iter.Pull(seq)
defer stop()
return next()
}
func PeekN[V any](seq iter.Seq[V], n int) []V {
result := make([]V, 0, n)
next, stop := iter.Pull(seq)
defer stop()
for i := 0; i < n; i++ {
v, ok := next()
if !ok {
break
}
result = append(result, v)
}
return result
}
nums := Countdown(100)
fmt.Println(First(nums))
fmt.Println(PeekN(nums, 5))
Важно: Очистка ресурсов iter.Pull
func ProcessLines(filename string) iter.Seq[string] {
return func(yield func(string) bool) {
file, err := os.Open(filename)
if err != nil {
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
if !yield(scanner.Text()) {
return
}
}
}
}
func main() {
next, stop := iter.Pull(ProcessLines("huge.log"))
defer stop()
v, ok := next()
if ok {
fmt.Println("First line:", v)
}
}
defer stop() гарантирует, что файл будет корректно закрыт, даже если получено только одно значение.
Паттерн 3: Композиция конвейеров данных (Map/Filter/Reduce)
Проблема: Вложенные циклы и промежуточные срезы
func ProcessOrders(orders []Order) float64 {
var active []Order
for _, o := range orders {
if o.Status == "active" {
active = append(active, o)
}
}
var amounts []float64
for _, o := range active {
amounts = append(amounts, o.Amount*1.1)
}
var total float64
for _, a := range amounts {
total += a
}
return total
}
3 обхода, 2 промежуточных среза. При больших объёмах данных нагрузка на память и GC значительна.
Решение: Конвейер итераторов
package pipeline
import "iter"
func Map[V any, U any](seq iter.Seq[V], fn func(V) U) iter.Seq[U] {
return func(yield func(U) bool) {
for v := range seq {
if !yield(fn(v)) {
return
}
}
}
}
func Map2[K any, V any, U any](seq iter.Seq2[K, V], fn func(K, V) U) iter.Seq[U] {
return func(yield func(U) bool) {
for k, v := range seq {
if !yield(fn(k, v)) {
return
}
}
}
}
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if pred(v) {
if !yield(v) {
return
}
}
}
}
}
func Filter2[K any, V any](seq iter.Seq2[K, V], pred func(K, V) bool) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range seq {
if pred(k, v) {
if !yield(k, v) {
return
}
}
}
}
}
func Reduce[V any, U any](seq iter.Seq[V], init U, fn func(U, V) U) U {
acc := init
for v := range seq {
acc = fn(acc, v)
}
return acc
}
Использование композиции конвейеров
type Order struct {
ID int
Status string
Amount float64
}
func OrdersFromDB(db *sql.DB) iter.Seq[Order] {
return func(yield func(Order) bool) {
rows, _ := db.Query("SELECT id, status, amount FROM orders")
if rows != nil {
defer rows.Close()
for rows.Next() {
var o Order
rows.Scan(&o.ID, &o.Status, &o.Amount)
if !yield(o) {
return
}
}
}
}
}
func main() {
db, _ := sql.Open("postgres", "dsn")
total := Reduce(
Map(
Filter(
OrdersFromDB(db),
func(o Order) bool { return o.Status == "active" },
),
func(o Order) float64 { return o.Amount * 1.1 },
),
0.0,
func(acc float64, v float64) float64 { return acc + v },
)
fmt.Printf("Total: %.2f\n", total)
}
Нулевое промежуточное выделение памяти: Filter, Map и Reduce объединены в цепочку — каждый элемент обрабатывается ровно один раз.
Поток выполнения конвейера
OrdersFromDB → Filter(active) → Map(×1.1) → Reduce(+) → total
│ │ │ │
│ Order{1, │ Status== │ Amount*1.1 │ acc+v
│ "active", │ "active"? │ │
│ 100.0} │ │ │
│ ──────→ │ ✓ pass │ │
│ │ ──────→ │ 110.0 │
│ │ │ ──────→ │ 110.0
│
│ Order{2, │ Status!= │ │
│ "closed", │ "active" │ │
│ 200.0} │ ✗ filtered │ │
│ ──────→ │ skip │ │
Дополнительные операторы конвейера
func FlatMap[V any, U any](seq iter.Seq[V], fn func(V) iter.Seq[U]) iter.Seq[U] {
return func(yield func(U) bool) {
for v := range seq {
for u := range fn(v) {
if !yield(u) {
return
}
}
}
}
}
func Zip[V any, U any](seq1 iter.Seq[V], seq2 iter.Seq[U]) iter.Seq2[V, U] {
return func(yield func(V, U) bool) {
next1, stop1 := iter.Pull(seq1)
defer stop1()
next2, stop2 := iter.Pull(seq2)
defer stop2()
for {
v1, ok1 := next1()
v2, ok2 := next2()
if !ok1 || !ok2 {
return
}
if !yield(v1, v2) {
return
}
}
}
}
func Enumerate[V any](seq iter.Seq[V]) iter.Seq2[int, V] {
return func(yield func(int, V) bool) {
i := 0
for v := range seq {
if !yield(i, v) {
return
}
i++
}
}
}
func Chunk[V any](seq iter.Seq[V], size int) iter.Seq[[]V] {
return func(yield func([]V) bool) {
chunk := make([]V, 0, size)
for v := range seq {
chunk = append(chunk, v)
if len(chunk) == size {
if !yield(chunk) {
return
}
chunk = make([]V, 0, size)
}
}
if len(chunk) > 0 {
yield(chunk)
}
}
}
Паттерн 4: Ленивые вычисления и бесконечные последовательности
Проблема: Растрата на предварительные вычисления всех результатов
func Fibonacci(n int) []int {
result := make([]int, n)
if n > 0 {
result[0] = 0
}
if n > 1 {
result[1] = 1
}
for i := 2; i < n; i++ {
result[i] = result[i-1] + result[i-2]
}
return result
}
Нужны первые 10 чисел Фибоначчи? Необходимо указать n. Не знаете сколько? Вычислите «достаточно большое» значение заранее.
Решение: Бесконечный итератор + ленивые вычисления
package lazy
import "iter"
func Fibonacci() iter.Seq[int] {
return func(yield func(int) bool) {
a, b := 0, 1
for {
if !yield(a) {
return
}
a, b = b, a+b
}
}
}
func NaturalNumbers() iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; ; i++ {
if !yield(i) {
return
}
}
}
}
func Repeat[V any](v V) iter.Seq[V] {
return func(yield func(V) bool) {
for {
if !yield(v) {
return
}
}
}
}
func Iterate[V any](init V, fn func(V) V) iter.Seq[V] {
return func(yield func(V) bool) {
v := init
for {
if !yield(v) {
return
}
v = fn(v)
}
}
}
func Cycle[V any](seq iter.Seq[V]) iter.Seq[V] {
return func(yield func(V) bool) {
for {
for v := range seq {
if !yield(v) {
return
}
}
}
}
}
Использование ленивых последовательностей
func main() {
for v := range Take(Fibonacci(), 10) {
fmt.Print(v, " ")
}
fmt.Println()
squares := Map(
Take(NaturalNumbers(), 5),
func(n int) int { return n * n },
)
for v := range squares {
fmt.Print(v, " ")
}
fmt.Println()
powersOf2 := Iterate(1, func(v int) int { return v * 2 })
for v := range Take(powersOf2, 8) {
fmt.Print(v, " ")
}
fmt.Println()
}
Ленивая обработка файлов
func FileLines(path string) iter.Seq[string] {
return func(yield func(string) bool) {
f, err := os.Open(path)
if err != nil {
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
if !yield(scanner.Text()) {
return
}
}
}
}
func Grep(pattern string, lines iter.Seq[string]) iter.Seq[string] {
re := regexp.MustCompile(pattern)
return Filter(lines, func(line string) bool {
return re.MatchString(line)
})
}
func main() {
errors := Grep("ERROR", FileLines("/var/log/app.log"))
for line := range Take(errors, 100) {
fmt.Println(line)
}
}
Лог-файл размером 10 ГБ? Читаются только первые 100 строк с ERROR — использование памяти почти нулевое.
Паттерн 5: Параллельные итераторы и Fan-out
Проблема: Узкое место производительности однопоточного итератора
func ProcessImages(images iter.Seq[Image]) []Result {
var results []Result
for img := range images {
r := expensiveTransform(img)
results = append(results, r)
}
return results
}
1000 изображений, по 100 мс каждое, всего 100 секунд. Утилизация CPU всего 12,5% (используется 1 из 8 ядер).
Решение: Параллельный итератор Fan-out
package concurrent
import (
"iter"
"sync"
)
func FanOut[V any, U any](seq iter.Seq[V], workers int, fn func(V) U) iter.Seq[U] {
return func(yield func(U) bool) {
inputCh := make(chan V)
outputCh := make(chan U)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for v := range inputCh {
outputCh <- fn(v)
}
}()
}
go func() {
for v := range seq {
inputCh <- v
}
close(inputCh)
wg.Wait()
close(outputCh)
}()
for u := range outputCh {
if !yield(u) {
return
}
}
}
}
func FanOutOrdered[V any, U any](seq iter.Seq[V], workers int, fn func(V) U) iter.Seq[U] {
return func(yield func(U) bool) {
type indexedResult struct {
index int
value U
}
next, stop := iter.Pull(seq)
defer stop()
type indexedInput[V any] struct {
index int
value V
}
inputCh := make(chan indexedInput[V], workers)
outputCh := make(chan indexedResult, workers)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for inp := range inputCh {
outputCh <- indexedResult{
index: inp.index,
value: fn(inp.value),
}
}
}()
}
go func() {
idx := 0
for {
v, ok := next()
if !ok {
break
}
inputCh <- indexedInput[V]{index: idx, value: v}
idx++
}
close(inputCh)
wg.Wait()
close(outputCh)
}()
results := make(map[int]U)
nextIdx := 0
for res := range outputCh {
results[res.index] = res.value
for {
r, ok := results[nextIdx]
if !ok {
break
}
delete(results, nextIdx)
if !yield(r) {
return
}
nextIdx++
}
}
}
}
Использование параллельных итераторов
type Image struct {
Path string
Data []byte
}
type Result struct {
Path string
Thumbnail []byte
}
func LoadImages(paths iter.Seq[string]) iter.Seq[Image] {
return Map(paths, func(p string) Image {
data, _ := os.ReadFile(p)
return Image{Path: p, Data: data}
})
}
func expensiveTransform(img Image) Result {
thumbnail := resizeImage(img.Data, 100, 100)
return Result{Path: img.Path, Thumbnail: thumbnail}
}
func main() {
paths := SliceIterator([]string{"a.jpg", "b.jpg", "c.jpg"})
results := FanOut(
LoadImages(paths),
runtime.NumCPU(),
expensiveTransform,
)
for r := range results {
fmt.Printf("Done: %s\n", r.Path)
}
}
Архитектура параллельного итератора
┌──────────┐
│ Seq[V] │
│ (source) │
└────┬─────┘
│
┌─────▼─────┐
│ inputCh │
└─────┬─────┘
│
┌───────────┼───────────┐
│ │ │
┌────▼───┐ ┌───▼────┐ ┌──▼─────┐
│Worker 1│ │Worker 2│ │Worker N│
│ fn(v) │ │ fn(v) │ │ fn(v) │
└────┬───┘ └───┬────┘ └──┬─────┘
│ │ │
└───────────┼──────────┘
│
┌─────▼─────┐
│ outputCh │
└─────┬─────┘
│
┌─────▼─────┐
│ yield(U) │
│ (consumer)│
└───────────┘
Паттерн 6: Обработка ошибок в итераторах
Проблема: Ошибки, поглощённые в итераторах
func ReadRecords(path string) iter.Seq[Record] {
return func(yield func(Record) bool) {
file, _ := os.Open(path)
defer file.Close()
decoder := json.NewDecoder(file)
for decoder.More() {
var r Record
if err := decoder.Decode(&r); err != nil {
return
}
if !yield(r) {
return
}
}
}
}
Когда decoder.Decode завершается ошибкой, она полностью теряется. Вызывающий код не может определить, завершилась ли итерация нормально или из-за ошибки.
Решение: Итераторы с распространением ошибок
package itererr
import "iter"
type Result[V any] struct {
Value V
Err error
}
func SeqWithError[V any](seq iter.Seq[Result[V]]) (iter.Seq[V], *error) {
var firstErr error
values := func(yield func(V) bool) {
for r := range seq {
if r.Err != nil {
if firstErr == nil {
firstErr = r.Err
}
return
}
if !yield(r.Value) {
return
}
}
}
return values, &firstErr
}
func Wrap[V any](seq iter.Seq[V], errPtr *error) iter.Seq[Result[V]] {
return func(yield func(Result[V]) bool) {
for v := range seq {
if *errPtr != nil {
return
}
if !yield(Result[V]{Value: v}) {
return
}
}
}
}
Практическое применение: Итератор строк базы данных
package dbiter
import (
"database/sql"
"iter"
)
type RowResult[T any] struct {
Value T
Err error
}
func QueryRows[T any](db *sql.DB, query string, scan func(*sql.Rows) (T, error)) iter.Seq[RowResult[T]] {
return func(yield func(RowResult[T]) bool) {
rows, err := db.Query(query)
if err != nil {
yield(RowResult[T]{Err: err})
return
}
defer rows.Close()
for rows.Next() {
v, err := scan(rows)
if err != nil {
yield(RowResult[T]{Err: err})
return
}
if !yield(RowResult[T]{Value: v}) {
return
}
}
if err := rows.Err(); err != nil {
yield(RowResult[T]{Err: err})
}
}
}
type Product struct {
ID int
Name string
Price float64
}
func main() {
db, _ := sql.Open("postgres", "dsn")
products := QueryRows(db,
"SELECT id, name, price FROM products",
func(rows *sql.Rows) (Product, error) {
var p Product
err := rows.Scan(&p.ID, &p.Name, &p.Price)
return p, err
},
)
for r := range products {
if r.Err != nil {
log.Printf("Iteration error: %v", r.Err)
break
}
fmt.Printf("%s: $%.2f\n", r.Value.Name, r.Value.Price)
}
}
Конвейер распространения ошибок
func SafeMap[V any, U any](seq iter.Seq[RowResult[V]], fn func(V) (U, error)) iter.Seq[RowResult[U]] {
return func(yield func(RowResult[U]) bool) {
for r := range seq {
if r.Err != nil {
if !yield(RowResult[U]{Err: r.Err}) {
return
}
return
}
u, err := fn(r.Value)
if err != nil {
if !yield(RowResult[U]{Err: err}) {
return
}
return
}
if !yield(RowResult[U]{Value: u}) {
return
}
}
}
}
func SafeFilter[V any](seq iter.Seq[RowResult[V]], pred func(V) (bool, error)) iter.Seq[RowResult[V]] {
return func(yield func(RowResult[V]) bool) {
for r := range seq {
if r.Err != nil {
if !yield(r) {
return
}
return
}
ok, err := pred(r.Value)
if err != nil {
if !yield(RowResult[V]{Err: err}) {
return
}
return
}
if ok {
if !yield(r) {
return
}
}
}
}
}
Паттерн 7: Проектирование продакшен-библиотеки итераторов
Принципы проектирования
┌─────────────────────────────────────────────┐
│ Production Iterator Library Principles │
├─────────────────────────────────────────────┤
│ 1. Zero allocation: no intermediate slices │
│ 2. Composable: all ops return iter.Seq │
│ 3. Terminable: release on yield=false │
│ 4. Observable: error propagation & metrics │
│ 5. Testable: pure functions, no side effects│
└─────────────────────────────────────────────┘
Полный набор инструментов итераторов
package itool
import "iter"
type Seq[V any] = iter.Seq[V]
type Seq2[K any, V any] = iter.Seq2[K, V]
func FromSlice[V any](s []V) Seq[V] {
return func(yield func(V) bool) {
for _, v := range s {
if !yield(v) {
return
}
}
}
}
func FromMap[K comparable, V any](m map[K]V) Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range m {
if !yield(k, v) {
return
}
}
}
}
func FromChannel[V any](ch <-chan V) Seq[V] {
return func(yield func(V) bool) {
for v := range ch {
if !yield(v) {
return
}
}
}
}
func Generate[V any](fn func() (V, bool)) Seq[V] {
return func(yield func(V) bool) {
for {
v, ok := fn()
if !ok || !yield(v) {
return
}
}
}
}
func Concat[V any](seqs ...Seq[V]) Seq[V] {
return func(yield func(V) bool) {
for _, seq := range seqs {
for v := range seq {
if !yield(v) {
return
}
}
}
}
}
func Distinct[V comparable](seq Seq[V]) Seq[V] {
return func(yield func(V) bool) {
seen := make(map[V]bool)
for v := range seq {
if !seen[v] {
seen[v] = true
if !yield(v) {
return
}
}
}
}
}
func Reverse[V any](seq Seq[V]) Seq[V] {
return func(yield func(V) bool) {
var items []V
for v := range seq {
items = append(items, v)
}
for i := len(items) - 1; i >= 0; i-- {
if !yield(items[i]) {
return
}
}
}
}
func Skip[V any](seq Seq[V], n int) Seq[V] {
return func(yield func(V) bool) {
i := 0
for v := range seq {
if i >= n {
if !yield(v) {
return
}
}
i++
}
}
}
func TakeWhile[V any](seq Seq[V], pred func(V) bool) Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if !pred(v) {
return
}
if !yield(v) {
return
}
}
}
}
func SkipWhile[V any](seq Seq[V], pred func(V) bool) Seq[V] {
return func(yield func(V) bool) {
skipping := true
for v := range seq {
if skipping {
if pred(v) {
continue
}
skipping = false
}
if !yield(v) {
return
}
}
}
}
func Count[V any](seq Seq[V]) int {
n := 0
for range seq {
n++
}
return n
}
func Any[V any](seq Seq[V], pred func(V) bool) bool {
for v := range seq {
if pred(v) {
return true
}
}
return false
}
func All[V any](seq Seq[V], pred func(V) bool) bool {
for v := range seq {
if !pred(v) {
return false
}
}
return true
}
func ForEach[V any](seq Seq[V], fn func(V)) {
for v := range seq {
fn(v)
}
}
func ToSlice[V any](seq Seq[V]) []V {
var result []V
for v := range seq {
result = append(result, v)
}
return result
}
func ToMap[K comparable, V any](seq Seq2[K, V]) map[K]V {
result := make(map[K]V)
for k, v := range seq {
result[k] = v
}
return result
}
func GroupBy[K comparable, V any](seq Seq2[K, V]) map[K][]V {
result := make(map[K][]V)
for k, v := range seq {
result[k] = append(result[k], v)
}
return result
}
Примеры использования
func main() {
nums := FromSlice([]int{1, 2, 3, 4, 5, 4, 3, 2, 1})
result := ToSlice(
Distinct(
Filter(
Map(nums, func(n int) int { return n * 2 }),
func(n int) bool { return n > 4 },
),
),
)
fmt.Println(result)
evenCount := Count(Filter(FromSlice([]int{1, 2, 3, 4, 5, 6}), func(n int) bool {
return n%2 == 0
}))
fmt.Println("Even count:", evenCount)
hasNegative := Any(FromSlice([]int{1, 2, 3}), func(n int) bool {
return n < 0
})
fmt.Println("Has negative:", hasNegative)
}
5 распространённых ошибок и решения
Ошибка 1: Захват переменных цикла в итераторах
func BuggyFactory() []iter.Seq[int] {
var seqs []iter.Seq[int]
for i := 0; i < 3; i++ {
seqs = append(seqs, func(yield func(int) bool) {
yield(i)
})
}
return seqs
}
Все итераторы возвращают 3. i захватывается замыканием, значение равно 3 после завершения цикла.
Исправление:
func FixedFactory() []iter.Seq[int] {
var seqs []iter.Seq[int]
for i := 0; i < 3; i++ {
i := i
seqs = append(seqs, func(yield func(int) bool) {
yield(i)
})
}
return seqs
}
Ошибка 2: Забытый вызов stop вызывает утечку ресурсов
next, stop := iter.Pull(FileLines("big.log"))
v, ok := next()
fmt.Println(v)
Файл никогда не закрывается.
Исправление:
next, stop := iter.Pull(FileLines("big.log"))
defer stop()
v, ok := next()
fmt.Println(v)
Ошибка 3: Итераторы не реентерабельны
seq := Fibonacci()
for v := range Take(seq, 5) {
fmt.Println(v)
}
for v := range Take(seq, 5) {
fmt.Println(v)
}
Второй range ничего не выводит. Итераторы одноразовые.
Исправление:
fibFactory := func() iter.Seq[int] { return Fibonacci() }
for v := range Take(fibFactory(), 5) {
fmt.Println(v)
}
for v := range Take(fibFactory(), 5) {
fmt.Println(v)
}
Ошибка 4: Паника в итераторах
func RiskySeq() iter.Seq[int] {
return func(yield func(int) bool) {
panic("oops")
}
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
for v := range RiskySeq() {
fmt.Println(v)
}
}
В Go 1.24 паники из range over func могут быть восстановлены извне. Но не полагайтесь на это поведение — итераторы должны обрабатывать свои собственные ошибки.
Ошибка 5: Параллельный range по одному итератору
seq := FromSlice([]int{1, 2, 3, 4, 5})
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for v := range seq {
fmt.Println(v)
}
}()
}
wg.Wait()
iter.Seq не является потокобезопасным. Одновременный range из нескольких горутин вызывает гонку данных.
Исправление: Используйте паттерн Fan-out или создавайте независимые итераторы для каждой горутины.
10 распространённых ошибок и способы их устранения
| Ошибка | Причина | Решение |
|---|---|---|
cannot range over seq (variable of type func(yield func(int) bool)) |
Сигнатура функции не соответствует iter.Seq | Убедитесь, что сигнатура func(yield func(V) bool) |
cannot use function as type iter.Seq[int] |
Несоответствие типа параметра функции yield | Проверьте, что тип параметра yield соответствует параметру типа Seq |
iter.Pull: iterator did not call stop |
Функция stop Pull-итератора не вызвана | Всегда используйте defer stop() |
panic: range over func: yield called after return |
yield вызван после возврата итератора | Проверьте, не вызывают ли горутины yield после возврата |
deadlock |
inputCh/outputCh Fan-out не закрыты | Убедитесь, что все горутины завершились перед закрытием каналов |
data race |
Несколько горутин выполняют range по одному Seq | Каждая горутина использует независимый итератор |
out of memory |
Бесконечный итератор без Take | Всегда используйте Take/Skip с бесконечными последовательностями |
goroutine leak |
Горутины в итераторе никогда не завершаются | Используйте context или done-канал для управления выходом |
unexpected EOF during iteration |
Файл изменён во время итерации | Используйте файловые блокировки или снимки |
yield returns false but iteration continues |
Возвращаемое значение yield не проверяется | Проверяйте возвращаемое значение yield после каждого вызова, возвращайте при false |
Продвинутые советы по оптимизации
Совет 1: Предварительное выделение для снижения нагрузки на GC
func ToSlicePrealloc[V any](seq Seq[V], hint int) []V {
result := make([]V, 0, hint)
for v := range seq {
result = append(result, v)
}
return result[:len(result)]
}
Если вы знаете приблизительное количество, предварительно выделите память, чтобы избежать множественных расширений.
Совет 2: Пакетный итератор для сокращения системных вызовов
func Batched[V any](seq Seq[V], batchSize int) Seq[[]V] {
return func(yield func([]V) bool) {
batch := make([]V, 0, batchSize)
for v := range seq {
batch = append(batch, v)
if len(batch) == batchSize {
if !yield(batch) {
return
}
batch = make([]V, 0, batchSize)
}
}
if len(batch) > 0 {
yield(batch)
}
}
}
Для пакетных вставок в базу данных фиксируйте каждые 100 строк, чтобы сократить количество сетевых round trip.
Совет 3: Итератор + Context для контроля тайм-аута
func WithContext[V any](ctx context.Context, seq Seq[V]) Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
select {
case <-ctx.Done():
return
default:
if !yield(v) {
return
}
}
}
}
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
for v := range WithContext(ctx, SlowIterator()) {
fmt.Println(v)
}
Сравнительный анализ: Итераторы vs Каналы vs Срезы
| Аспект | Итератор (iter.Seq) | Канал (chan) | Срез ([]T) |
|---|---|---|---|
| Использование памяти | O(1) | O(n) буфер | O(n) |
| Ленивые вычисления | Да | Нет | Нет |
| Бесконечные последовательности | Да | Нет | Нет |
| Потокобезопасность | Нет | Да | Нет |
| Компонуемость | Отличная (цепочки функций) | Средняя (нужна горутина) | Плохая (промежуточные срезы) |
| Обработка ошибок | Требует обёртки | Нативная поддержка | Прямой возврат ошибки |
| Реентерабельность | Нет | Нет | Да |
| Производительность | Нулевое выделение | Накладные расходы на блокировки | Накладные расходы на копирование |
| Случай использования | Конвейеры данных, ленивые вычисления | Параллельное взаимодействие | Малые наборы данных, произвольный доступ |
| Версия Go | 1.24+ | 1.0+ | 1.0+ |
| Сложность отладки | Средняя | Высокая | Низкая |
Дерево решений
Need lazy evaluation or infinite sequences?
├── Yes → Iterator
└── No
├── Need concurrent communication?
│ └── Yes → Channel
└── No
├── Small data + random access?
│ └── Yes → Slice
└── No → Iterator
Рекомендуемые онлайн-инструменты
- JSON Форматер — Форматирование JSON вывода из итераторов
- Форматер кода — Форматирование кода итераторов Go
- SQL Форматер — Форматирование SQL запросов в итераторах
Внешние ссылки
- Документация пакета Go iter — Справочник по пакету iter стандартной библиотеки Go
- Предложение Go Range Over Func — Документ с дизайном range over func
Итоги
Паттерны итераторов Go 1.24 предоставляют Go нативные возможности конвейеров данных с нулевым выделением памяти и компонуемостью. 7 основных паттернов охватывают весь спектр от базового обхода до проектирования продакшен-библиотек:
- Базовый итератор range over func — Отправная точка, yield управляет потоком
- Преобразование Push/Pull итераторов — iter.Pull обеспечивает ручное управление
- Композиция конвейеров данных — Цепочки Map/Filter/Reduce с нулевым промежуточным выделением памяти
- Ленивые вычисления и бесконечные последовательности — Вычисления по требованию, обработка бесконечных потоков
- Параллельные итераторы и Fan-out — Параллельное потребление несколькими горутинами
- Обработка ошибок в итераторах — Паттерн RowResult для изящного распространения ошибок
- Проектирование продакшен-библиотеки итераторов — Нулевое выделение, компонуемость, завершаемость
Итераторы — это не замена каналам или срезам. Это новый член набора инструментов обработки данных Go — когда вам нужны ленивые вычисления и компоновка конвейеров с нулевым выделением памяти, итераторы — лучший выбор.
Рекомендуемое чтение:
Попробуйте эти локальные браузерные инструменты — регистрация не требуется →