Patterns d'itérateurs Go 1.24 : 7 patterns de production de range over func aux pipelines de données

编程语言(Mis à jour le 14 juil. 2026)

Quand les boucles for rencontrent les fonctions : le changement de paradigme des itérateurs Go

La semaine dernière, j'ai refactoré un service de traitement de données avec 3 niveaux de boucles for imbriquées traitant 100K enregistrements — la mémoire a grimpé à 2 Go car chaque niveau devait stocker les résultats intermédiaires sous forme de slices avant de les passer au suivant. Après être passé à un pipeline d'itérateurs, la mémoire est tombée à 15 Mo et le traitement était 30 % plus rapide. Le changement clé : plus de « collecter puis traiter » — mais « itérer et traiter à la volée ».

Go 1.24 a officiellement stabilisé la syntaxe range over func et le package iter, offrant à Go des itérateurs natifs, sans allocation et composables. Ce n'est pas du sucre syntaxique — c'est un changement fondamental dans les paradigmes de traitement de données. Cet article couvre 7 patterns d'itérateurs Go de qualité production pour vous aider à construire des pipelines de données efficaces, élégants et composables.


Points clés

  • range over func est la syntaxe d'itérateur centrale : Go 1.24 permet d'itérer directement sur des fonctions avec range
  • Conversion d'itérateurs Push/Pull : Comprendre les deux directions, maîtriser la conversion iter.Pull
  • Composition de pipelines de données : Composition en chaîne Map/Filter/Reduce sans allocation intermédiaire
  • Évaluation paresseuse et séquences infinies : Calcul à la demande, traitement de flux de données infinis
  • Itérateurs concurrents et Fan-out : Plusieurs goroutines consommant des itérateurs en parallèle
  • Gestion des erreurs d'itérateur : Gérer élégamment les erreurs pendant l'itération
  • Conception de bibliothèque d'itérateurs de qualité production : Construire des boîtes à outils d'itérateurs réutilisables

Table des matières

  1. Référence des concepts fondamentaux des itérateurs Go
  2. Pattern 1 : Itérateur basique range over func
  3. Pattern 2 : Conversion d'itérateurs Push/Pull
  4. Pattern 3 : Composition de pipelines de données (Map/Filter/Reduce)
  5. Pattern 4 : Évaluation paresseuse et séquences infinies
  6. Pattern 5 : Itérateurs concurrents et Fan-out
  7. Pattern 6 : Gestion des erreurs d'itérateur
  8. Pattern 7 : Conception de bibliothèque d'itérateurs de qualité production
  9. 5 pièges courants et solutions
  10. 10 résolutions d'erreurs courantes
  11. Conseils d'optimisation avancés
  12. Analyse comparative : Itérateurs vs Canaux vs Slices
  13. Outils en ligne recommandés
  14. Résumé

Référence des concepts fondamentaux des itérateurs Go

Concept Signature Objectif Exemple
Fonction itérateur func(yield func(V) bool) Itérateur à valeur unique func(yield func(int) bool)
Itérateur clé-valeur func(yield func(K, V) bool) Itération de paires clé-valeur func(yield func(int, string) bool)
Itérateur Pull func() (V, bool) Tirage à la demande next, stop := iter.Pull(seq)
iter.Pull func(Seq[V]) (func() (V, bool), func()) Conversion Push vers Pull Parcours piloté par le consommateur
iter.Stop Fonction d'arrêt intégrée Terminaison anticipée stop() libère les ressources
Valeur de retour yield bool Contrôler la poursuite/arrêt de l'itération yield(v) retourne false pour arrêter
Évaluation paresseuse Calcul différé Générer les valeurs à la demande Séquences infinies, lignes de fichier
Composition de pipeline Enchaînement de fonctions Zéro allocation intermédiaire Filter(Map(Seq, fn), pred)

Pattern 1 : Itérateur basique range over func

Problème : Les pièges mémoire du parcours traditionnel

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 million d'utilisateurs ? 1 million de structs User chargés en mémoire. Vous n'avez besoin que des 10 premiers ? Tant pis — chargez-les tous d'abord.

Solution : Itérateur 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++
        }
    }
}

Utilisation :

for i, user := range AllUsers(db) {
    fmt.Printf("%d: %s\n", i, user.Name)
    if i >= 9 {
        break
    }
}

Quand break s'exécute, yield retourne false, et la fonction itérateur retourne immédiatement — seulement 10 lignes interrogées, connexion à la base de données correctement fermée.

Flux d'exécution de l'itérateur

┌─────────────┐     yield(v)      ┌──────────────┐
│  Itérateur   │ ──────────────→  │  boucle range │
│  (producteur)│                   │  (consommateur)│
│              │  ←──────────────  │              │
│              │  yield retourne   │              │
│              │      bool         │              │
└─────────────┘                   └──────────────┘
      │                                  │
      │  yield retourne false → return   │
      │  (terminaison anticipée)         │
      └──────────────────────────────────┘

Itérateur à valeur unique vs clé-valeur

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)
}

Pattern 2 : Conversion d'itérateurs Push/Pull

Différences entre itérateurs Push et Pull

Itérateur Push (iter.Seq)            Itérateur Pull (func() (V, bool))
┌──────────────────┐               ┌──────────────────┐
│  Producteur pousse│               │  Consommateur tire│
│  yield(v) → cons. │               │  next() → prod.   │
│                    │               │                    │
│  Bon pour : range  │               │  Bon pour : manuel │
│  Bon pour : pipes  │               │  Bon pour : peek   │
│  Bon pour : lazy   │               │  Bon pour : interop│
└──────────────────┘               └──────────────────┘
         │                                  │
         │      conversion iter.Pull()      │
         └──────────────────────────────────┘

Utilisation de la 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
        }
    }
}

Applications pratiques d'itérateurs Pull : Peek et 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))

Important : Nettoyage des ressources 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() garantit que le fichier est correctement fermé même si une seule valeur est consommée.


Pattern 3 : Composition de pipelines de données (Map/Filter/Reduce)

Problème : Boucles imbriquées et slices intermédiaires

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 parcours, 2 slices intermédiaires. Avec des jeux de données volumineux, la mémoire et la pression GC sont significatives.

Solution : Pipeline d'itérateurs

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
}

Utilisation de la composition de pipeline

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)
}

Zéro allocation intermédiaire : Filter, Map et Reduce sont chaînés — chaque élément est traité exactement une fois.

Flux d'exécution du pipeline

OrdersFromDB → Filter(active) → Map(×1.1) → Reduce(+) → total
     │              │                │            │
     │  Order{1,    │  Status==      │  Amount*1.1 │  acc+v
     │  "active",   │  "active"?     │             │
     │  100.0}      │                │             │
     │      ──────→ │  ✓ passe       │             │
     │              │      ──────→   │  110.0      │
     │              │                │   ──────→   │  110.0
     │
     │  Order{2,    │  Status!=      │             │
     │  "closed",   │  "active"      │             │
     │  200.0}      │  ✗ filtré      │             │
     │      ──────→ │  ignoré        │             │

Plus d'opérateurs de pipeline

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)
        }
    }
}

Pattern 4 : Évaluation paresseuse et séquences infinies

Problème : Le gaspillage du pré-calcul de tous les résultats

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
}

Vous avez besoin des 10 premiers nombres de Fibonacci ? Vous devez spécifier n. Vous ne savez pas combien ? Calculez une valeur « suffisamment grande » à l'avance.

Solution : Itérateur infini + évaluation paresseuse

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
                }
            }
        }
    }
}

Utilisation des séquences paresseuses

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()
}

Traitement paresseux de fichiers

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)
    }
}

Un fichier de log de 10 Go ? Seules les 100 premières lignes ERROR sont lues — utilisation mémoire quasi nulle.


Pattern 5 : Itérateurs concurrents et Fan-out

Problème : Goulot d'étranglement de performance des itérateurs mono-thread

func ProcessImages(images iter.Seq[Image]) []Result {
    var results []Result
    for img := range images {
        r := expensiveTransform(img)
        results = append(results, r)
    }
    return results
}

1000 images, 100 ms chacune, total 100 secondes. Utilisation CPU seulement 12,5 % (1 cœur sur 8 utilisé).

Solution : Itérateur concurrent 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++
            }
        }
    }
}

Utilisation des itérateurs concurrents

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)
    }
}

Architecture d'itérateur concurrent

               ┌──────────┐
               │ Seq[V]   │
               │ (source) │
               └────┬─────┘
                    │
              ┌─────▼─────┐
              │ inputCh   │
              └─────┬─────┘
                    │
        ┌───────────┼───────────┐
        │           │           │
   ┌────▼───┐  ┌───▼────┐  ┌──▼─────┐
   │Worker 1│  │Worker 2│  │Worker N│
   │ fn(v)  │  │ fn(v)  │  │ fn(v)  │
   └────┬───┘  └───┬────┘  └──┬─────┘
        │          │          │
        └───────────┼──────────┘
                    │
              ┌─────▼─────┐
              │ outputCh  │
              └─────┬─────┘
                    │
              ┌─────▼─────┐
              │ yield(U)  │
              │ (consommateur)│
              └───────────┘

Pattern 6 : Gestion des erreurs d'itérateur

Problème : Erreurs avalées dans les itérateurs

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
            }
        }
    }
}

Quand decoder.Decode échoue, l'erreur est complètement perdue. L'appelant ne peut pas distinguer si l'itération s'est terminée normalement ou à cause d'une erreur.

Solution : Itérateurs avec propagation d'erreurs

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
            }
        }
    }
}

Application pratique : Itérateur de lignes de base de données

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)
    }
}

Pipeline de propagation d'erreurs

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
                }
            }
        }
    }
}

Pattern 7 : Conception de bibliothèque d'itérateurs de qualité production

Principes de conception

┌─────────────────────────────────────────────┐
│  Principes de bibliothèque d'itérateurs     │
│  de qualité production                      │
├─────────────────────────────────────────────┤
│  1. Zéro allocation : pas de slices interm. │
│  2. Composable : toutes les ops retournent  │
│     iter.Seq                                │
│  3. Terminable : libération sur yield=false │
│  4. Observable : propagation d'erreurs &    │
│     métriques                               │
│  5. Testable : fonctions pures, pas d'effets│
│     de bord                                 │
└─────────────────────────────────────────────┘

Boîte à outils d'itérateurs complète

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
}

Exemples d'utilisation

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 pièges courants et solutions

Piège 1 : Capturer des variables de boucle dans les itérateurs

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
}

Tous les itérateurs retournent 3. i est capturé par closure, la valeur est 3 quand la boucle se termine.

Correction :

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
}

Piège 2 : Oublier d'appeler stop provoque des fuites de ressources

next, stop := iter.Pull(FileLines("big.log"))
v, ok := next()
fmt.Println(v)

Le fichier n'est jamais fermé.

Correction :

next, stop := iter.Pull(FileLines("big.log"))
defer stop()
v, ok := next()
fmt.Println(v)

Piège 3 : Les itérateurs ne sont pas réentrants

seq := Fibonacci()
for v := range Take(seq, 5) {
    fmt.Println(v)
}
for v := range Take(seq, 5) {
    fmt.Println(v)
}

Le deuxième range ne produit rien. Les itérateurs sont à usage unique.

Correction :

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)
}

Piège 4 : Panic dans les itérateurs

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)
    }
}

Dans Go 1.24, les panics provenant de range over func peuvent être récupérés en externe. Mais ne vous fiez pas à ce comportement — les itérateurs devraient gérer leurs propres erreurs.

Piège 5 : Range concurrentiel sur le même itérateur

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 n'est pas sûr pour la concurrence. Plusieurs goroutines itérant simultanément provoquent des courses de données.

Correction : Utilisez le pattern Fan-out, ou créez des itérateurs indépendants pour chaque goroutine.


10 résolutions d'erreurs courantes

Erreur Cause Solution
cannot range over seq (variable of type func(yield func(int) bool)) La signature de la fonction ne correspond pas à iter.Seq Assurez-vous que la signature est func(yield func(V) bool)
cannot use function as type iter.Seq[int] Incompatibilité du type de paramètre yield Vérifiez que le type de paramètre yield correspond au paramètre de type Seq
iter.Pull: iterator did not call stop L'arrêt de l'itérateur Pull n'est pas appelé Toujours defer stop()
panic: range over func: yield called after return yield appelé après le retour de l'itérateur Vérifiez si des goroutines appellent yield après le retour
deadlock inputCh/outputCh de Fan-out non fermés Assurez-vous que toutes les goroutines se terminent avant de fermer les canaux
data race Plusieurs goroutines itérant sur le même Seq Chaque goroutine utilise un itérateur indépendant
out of memory Itérateur infini sans Take Toujours utiliser Take/Skip avec les séquences infinies
goroutine leak Les goroutines dans l'itérateur ne se terminent jamais Utilisez context ou un canal done pour le contrôle de sortie
unexpected EOF during iteration Fichier modifié pendant l'itération du fichier Utilisez des verrous de fichier ou des instantanés
yield returns false but iteration continues Valeur de retour de yield non vérifiée Vérifiez le retour de yield après chaque appel, retournez sur false

Conseils d'optimisation avancés

Conseil 1 : Pré-allocation pour réduire la pression 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)]
}

Quand vous connaissez le nombre approximatif, pré-allouez pour éviter les redimensionnements multiples.

Conseil 2 : Itérateur par lots pour réduire les appels système

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)
        }
    }
}

Pour les insertions en base de données par lots, validez toutes les 100 lignes pour réduire les allers-retours réseau.

Conseil 3 : Itérateur + Context pour le contrôle du délai d'attente

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)
}

Analyse comparative : Itérateurs vs Canaux vs Slices

Dimension Itérateur (iter.Seq) Canal (chan) Slice ([]T)
Utilisation mémoire O(1) O(n) tampon O(n)
Évaluation paresseuse Oui Non Non
Séquences infinies Oui Non Non
Sécurité concurrentielle Non Oui Non
Composabilité Excellente (chaîne de fonctions) Moyenne (nécessite goroutine) Faible (slices intermédiaires)
Gestion des erreurs Nécessite un encapsulation Support natif Retour d'erreur direct
Réentrant Non Non Oui
Performance Zéro allocation Surcharge de verrou Surcharge de copie
Cas d'utilisation Pipelines de données, éval. paresseuse Communication concurrente Petits jeux de données, accès aléatoire
Version Go 1.24+ 1.0+ 1.0+
Difficulté de débogage Moyenne Élevée Faible

Arbre de décision

Besoin d'évaluation paresseuse ou de séquences infinies ?
├── Oui → Itérateur
└── Non
    ├── Besoin de communication concurrente ?
    │   └── Oui → Canal
    └── Non
        ├── Petites données + accès aléatoire ?
        │   └── Oui → Slice
        └── Non → Itérateur

Outils en ligne recommandés

Références externes


Résumé

Les patterns d'itérateurs Go 1.24 offrent à Go des capacités natives de pipelines de données sans allocation et composables. Les 7 patterns fondamentaux couvrent tout le spectre, du parcours basique à la conception de bibliothèque de qualité production :

  1. Itérateur basique range over func — Le point de départ, yield contrôle le flux
  2. Conversion d'itérateurs Push/Pull — iter.Pull permet le contrôle manuel
  3. Composition de pipelines de données — Chaînage Map/Filter/Reduce sans allocation intermédiaire
  4. Évaluation paresseuse et séquences infinies — Calcul à la demande, traitement de flux infinis
  5. Itérateurs concurrents et Fan-out — Consommation parallèle avec plusieurs goroutines
  6. Gestion des erreurs d'itérateur — Pattern RowResult pour une propagation gracieuse des erreurs
  7. Conception de bibliothèque d'itérateurs de qualité production — Zéro allocation, composable, terminable

Les itérateurs ne remplacent pas les canaux ou les slices. Ils sont un nouveau membre de la boîte à outils de traitement de données de Go — quand vous avez besoin d'évaluation paresseuse et de composition de pipeline sans allocation, les itérateurs sont le meilleur choix.

Lectures associées :

Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →

#Go#迭代器#Iterator#range over func#数据管道#Go 1.24#2026#编程语言