← Voltar para o blog

Iteradores em Go: Range Over Func Explicado

Aprenda iteradores em Go com range over func: iter.Seq, iter.Seq2, iter.Pull, iteradores customizados e composição de iteradores na prática.

O Go 1.23 trouxe uma das funcionalidades mais aguardadas da linguagem: range over functions, ou iteradores baseados em funções. Esse recurso permite usar for range sobre funções customizadas, abrindo um novo paradigma para processar sequências de dados em Go. Combinado com generics, os iteradores tornam o código mais expressivo sem sacrificar a performance.

O que Muda com Range Over Func?

Antes do Go 1.23, o for range funcionava apenas com tipos nativos: slices, maps, strings, channels e inteiros. Se você tinha uma estrutura de dados customizada, precisava expor o estado interno ou retornar um slice completo para iterar.

// Antes: expor slice interno ou criar um novo
func (t *Arvore) Todos() []Elemento {
    resultado := make([]Elemento, 0, t.tamanho)
    t.percorrer(t.raiz, &resultado)
    return resultado
}

// Iterar exigia alocar o slice inteiro na memória
for _, elem := range arvore.Todos() {
    processar(elem)
}

Com range over func, você itera diretamente sobre a estrutura sem alocações intermediárias:

// Depois: iterador sob demanda (lazy)
func (t *Arvore) Todos() iter.Seq[Elemento] {
    return func(yield func(Elemento) bool) {
        t.percorrer(t.raiz, yield)
    }
}

// Iteração direta, sem alocação extra
for elem := range arvore.Todos() {
    processar(elem)
}

Os Tipos iter.Seq e iter.Seq2

O pacote iter define dois tipos fundamentais para iteradores:

// Iterador que produz um valor por iteração
type Seq[V any] func(yield func(V) bool)

// Iterador que produz pares chave-valor
type Seq2[K, V any] func(yield func(K, V) bool)

A função yield recebe cada elemento e retorna bool: true para continuar iterando, false para parar (equivalente ao break no loop). Isso permite que o consumidor controle quando a iteração termina.

// iter.Seq — uma sequência de valores
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
        }
    }
}

// Uso com for range
for n := range Fibonacci() {
    if n > 1000 {
        break // yield retorna false, iterador para
    }
    fmt.Println(n)
}
// iter.Seq2 — pares chave-valor
func Enumerate[T any](s []T) iter.Seq2[int, T] {
    return func(yield func(int, T) bool) {
        for i, v := range s {
            if !yield(i, v) {
                return
            }
        }
    }
}

// Uso
for i, nome := range Enumerate([]string{"Ana", "Bruno", "Carlos"}) {
    fmt.Printf("%d: %s\n", i, nome)
}

Criando Iteradores Customizados

A beleza dos iteradores em Go é que qualquer estrutura de dados pode se tornar iterável. Veja um exemplo com uma lista encadeada:

type Node[T any] struct {
    Value T
    Next  *Node[T]
}

type LinkedList[T any] struct {
    Head *Node[T]
}

func (l *LinkedList[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        for node := l.Head; node != nil; node = node.Next {
            if !yield(node.Value) {
                return
            }
        }
    }
}

// Iterar é natural
for valor := range lista.All() {
    fmt.Println(valor)
}

Para árvores binárias com travessia in-order:

type TreeNode[T any] struct {
    Value       T
    Left, Right *TreeNode[T]
}

func (t *TreeNode[T]) InOrder() iter.Seq[T] {
    return func(yield func(T) bool) {
        var walk func(*TreeNode[T]) bool
        walk = func(n *TreeNode[T]) bool {
            if n == nil {
                return true
            }
            return walk(n.Left) && yield(n.Value) && walk(n.Right)
        }
        walk(t)
    }
}

Pull Iterators com iter.Pull

Nem sempre o modelo push (onde o iterador chama yield) é conveniente. Quando você precisa consumir valores um de cada vez ou intercalar duas sequências, use iter.Pull para converter um push iterator em pull iterator:

func MesclarOrdenado(a, b iter.Seq[int]) iter.Seq[int] {
    return func(yield func(int) bool) {
        nextA, stopA := iter.Pull(a)
        defer stopA()
        nextB, stopB := iter.Pull(b)
        defer stopB()

        va, okA := nextA()
        vb, okB := nextB()

        for okA && okB {
            if va <= vb {
                if !yield(va) { return }
                va, okA = nextA()
            } else {
                if !yield(vb) { return }
                vb, okB = nextB()
            }
        }

        for okA {
            if !yield(va) { return }
            va, okA = nextA()
        }
        for okB {
            if !yield(vb) { return }
            vb, okB = nextB()
        }
    }
}

O iter.Pull retorna duas funções: next (que retorna o próximo valor) e stop (que deve ser chamada com defer para liberar recursos). Esse padrão é essencial quando a lógica de consumo não se encaixa em um simples for range.

Iteradores na Biblioteca Padrão

O Go 1.23 e o Go 1.24 expandiram o uso de iteradores na biblioteca padrão. Os pacotes slices e maps agora oferecem funções que retornam iteradores:

import (
    "maps"
    "slices"
)

// slices.All — itera sobre índice e valor
for i, v := range slices.All([]string{"Go", "Rust", "Python"}) {
    fmt.Printf("%d: %s\n", i, v)
}

// slices.Values — apenas valores
for v := range slices.Values([]int{10, 20, 30}) {
    fmt.Println(v)
}

// slices.Backward — iteração reversa
for i, v := range slices.Backward([]string{"a", "b", "c"}) {
    fmt.Printf("%d: %s\n", i, v) // 2:c, 1:b, 0:a
}

// maps.Keys — iterar sobre chaves de um map
config := map[string]string{"host": "localhost", "porta": "8080"}
for chave := range maps.Keys(config) {
    fmt.Println(chave)
}

// maps.Values — iterar sobre valores
for valor := range maps.Values(config) {
    fmt.Println(valor)
}

// slices.Sorted — coletar iterador em slice ordenado
chaves := slices.Sorted(maps.Keys(config))

Composição de Iteradores

O verdadeiro poder dos iteradores aparece na composição. Você pode criar funções que transformam, filtram e combinam sequências:

// Filter — mantém apenas elementos que satisfazem a condição
func Filter[T any](seq iter.Seq[T], pred func(T) bool) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range seq {
            if pred(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

// Map — transforma cada elemento
func Map[T, U any](seq iter.Seq[T], fn func(T) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for v := range seq {
            if !yield(fn(v)) {
                return
            }
        }
    }
}

// Take — limita a quantidade de elementos
func Take[T any](seq iter.Seq[T], n int) iter.Seq[T] {
    return func(yield func(T) bool) {
        count := 0
        for v := range seq {
            if count >= n {
                return
            }
            if !yield(v) {
                return
            }
            count++
        }
    }
}

Composição em ação:

// Pegar os 5 primeiros números de Fibonacci maiores que 10
for n := range Take(Filter(Fibonacci(), func(n int) bool {
    return n > 10
}), 5) {
    fmt.Println(n) // 13, 21, 34, 55, 89
}

Casos de Uso Reais

Paginação de API

Iteradores se encaixam naturalmente em cenários de paginação, onde você busca dados sob demanda:

func ListarUsuarios(client *http.Client, baseURL string) iter.Seq2[Usuario, error] {
    return func(yield func(Usuario, error) bool) {
        pagina := 1
        for {
            usuarios, temProxima, err := buscarPagina(client, baseURL, pagina)
            if err != nil {
                yield(Usuario{}, err)
                return
            }
            for _, u := range usuarios {
                if !yield(u, nil) {
                    return
                }
            }
            if !temProxima {
                return
            }
            pagina++
        }
    }
}

// Consumir transparentemente, sem se preocupar com paginação
for usuario, err := range ListarUsuarios(client, "https://api.exemplo.com/usuarios") {
    if err != nil {
        slog.Error("falha ao buscar usuário", slog.String("erro", err.Error()))
        break
    }
    processar(usuario)
}

Note como usamos slog para logging estruturado no tratamento de erros — uma combinação poderosa para observabilidade em produção.

Processamento de Arquivos Linha a Linha

func Linhas(r io.Reader) iter.Seq2[string, error] {
    return func(yield func(string, error) bool) {
        scanner := bufio.NewScanner(r)
        for scanner.Scan() {
            if !yield(scanner.Text(), nil) {
                return
            }
        }
        if err := scanner.Err(); err != nil {
            yield("", err)
        }
    }
}

file, _ := os.Open("dados.csv")
defer file.Close()

for linha, err := range Linhas(file) {
    if err != nil {
        log.Fatal(err)
    }
    // Processar cada linha sem carregar o arquivo inteiro
}

Iterando sobre Rows de Banco de Dados

func QueryIterator(db *sql.DB, query string, args ...any) iter.Seq2[*sql.Row, error] {
    return func(yield func(*sql.Row, error) bool) {
        rows, err := db.Query(query, args...)
        if err != nil {
            yield(nil, err)
            return
        }
        defer rows.Close()

        for rows.Next() {
            if !yield(rows, nil) {
                return
            }
        }
        if err := rows.Err(); err != nil {
            yield(nil, err)
        }
    }
}

Performance: Iteradores vs Slices

Iteradores baseados em funções têm performance comparável a loops tradicionais. O compilador do Go consegue inline a maioria dos iteradores simples, resultando em overhead praticamente zero:

AbordagemAlocaçõesTempo relativo
for range slice0 (slice já existe)1x
iter.Seq (simples)0~1.05x
iter.Seq (composição)0-1~1.1x
iter.Pull1 (goroutine interna)~1.3x
Retornar slice novo1+ (slice + elementos)~1.5x+

A principal vantagem dos iteradores sobre slices é que eles são lazy — processam elementos sob demanda. Para conjuntos de dados grandes ou infinitos, isso evita alocações desnecessárias e reduz o uso de memória significativamente.

Boas Práticas

  1. Sempre verifique o retorno de yield: se yield retorna false, pare imediatamente. Ignorar isso causa deadlocks com iter.Pull.

  2. Use defer para cleanup: se o iterador abre recursos (arquivos, conexões), garanta que eles sejam liberados mesmo com break.

  3. Prefira iter.Seq para sequências simples: use iter.Seq2 apenas quando os pares chave-valor são semanticamente relevantes.

  4. Evite iter.Pull quando possível: push iterators são mais eficientes. Use iter.Pull apenas quando a lógica exige consumo manual.

  5. Componha em vez de materializar: encadeie Filter, Map e Take em vez de criar slices intermediários.

Próximos Passos

Iteradores transformam a maneira como processamos dados em Go. O Go 1.24 e o Go 1.25 continuam expandindo o suporte a iteradores na biblioteca padrão, tornando-os cada vez mais idiomáticos.

Python tem conceito similar com generators — veja geradores e iteradores em Python. Rust possui iteradores nativos com a trait Iterator — confira em Rust Brasil.

Para dominar os fundamentos que sustentam iteradores, explore generics em Go e entenda como concorrência interage com iteradores em pipelines de dados paralelos.