Bem-vindos à aula sobre iteradores em Go, especificamente a novidade do range sobre funções. Esta é uma das adições mais aguardadas à linguagem, introduzida experimentalmente na versão 1.23 e consolidada nas versões seguintes. Com ela, podemos estender a sintaxe familiar de range para funcionar com funções customizadas, permitindo criar coleções e sequências que podem ser iteradas de forma elegante e eficiente.

Antes dessa mudança, o range funcionava apenas sobre tipos nativos como slices, arrays, mapas, strings e canais. Agora, qualquer função que siga um padrão específico pode ser usada com range, abrindo um leque de possibilidades para design de APIs e abstrações. Nesta aula, vamos entender o conceito, a sintaxe, os casos de uso e ver exemplos práticos.

A novidade de iteradores

Go sempre se orgulhou de sua simplicidade e de ter poucas palavras-chave e construções. A introdução de iteradores é uma evolução significativa, pois permite que bibliotecas e aplicações definam seus próprios tipos iteráveis sem precisar criar tipos complexos ou usar callbacks manualmente. A motivação principal é permitir que funções sejam usadas diretamente com range, o que torna o código mais limpo e expressivo.

O padrão de função iteradora em Go é definido como uma função que recebe uma função de retorno (chamada de yield) como argumento. Essa função de retorno é chamada para cada elemento da iteração. Se o yield retornar false, a iteração é interrompida. Existem duas formas principais: uma que produz apenas valores (como um slice) e outra que produz pares chave-valor (como um mapa). A assinatura típica é:

func(yield func(V) bool) bool

para iterar sobre valores de tipo V, e

func(yield func(K, V) bool) bool

para iterar sobre pares chave-valor. A função iteradora retorna um booleano indicando se a iteração foi concluída (normalmente true) ou se foi interrompida prematuramente.

range sobre funções

Para usar uma função iteradora com range, basta escrever range funcName (sem parênteses). O compilador reconhece que funcName é uma função e a chama internamente, passando uma função de retorno que recebe os valores e os entrega ao corpo do loop. Exemplo:

func main() {
    for v := range myIterator {
        fmt.Println(v)
    }
}

Se a função iteradora produzir dois valores (chave e valor), podemos usar duas variáveis no range:

for k, v := range myMapIterator {
    fmt.Println(k, v)
}

É importante notar que a função iteradora não pode ser chamada diretamente com go ou defer; ela é chamada de forma síncrona pelo runtime do range. Isso garante que a iteração seja eficiente e sem concorrência desnecessária.

Casos de uso

Os iteradores são extremamente versáteis e podem ser usados em diversas situações:

  • Estruturas de dados personalizadas: Se você criar uma árvore, uma lista encadeada ou outra estrutura, pode fornecer um iterador para percorrê-la de forma natural.
  • Geração de sequências: Sequências infinitas ou geradas sob demanda, como números de Fibonacci, podem ser representadas como iteradores, sem precisar armazenar todos os valores.
  • Leitura de arquivos ou bancos de dados: Em vez de carregar tudo na memória, você pode iterar linha por linha ou registro por registro usando um iterador.
  • Composição de operações: Funções como map, filter e reduce podem ser implementadas como iteradores, permitindo pipelines de processamento de dados.
  • Integração com a biblioteca padrão: A partir do Go 1.23, funções como slices.Collect e maps.Collect podem transformar iteradores em slices e mapas, respectivamente.

Exemplos

Vamos ver alguns exemplos práticos de implementação e uso de iteradores.

Exemplo 1: Iterador sobre uma sequência de números

Vamos criar um iterador que gera números pares até um limite.

package main

import "fmt"

func evenNumbers(limit int) func(func(int) bool) bool {
    return func(yield func(int) bool) bool {
        for i := 0; i <= limit; i += 2 {
            if !yield(i) {
                return false
            }
        }
        return true
    }
}

func main() {
    for n := range evenNumbers(10) {
        fmt.Println(n)
    }
}

Aqui, evenNumbers retorna uma função iteradora que recebe um yield e itera sobre os números pares. O loop for n := range evenNumbers(10) imprime 0, 2, 4, 6, 8, 10.

Exemplo 2: Iterador sobre uma árvore binária

Vamos criar uma árvore binária simples e um iterador que percorre em ordem (in-order).

package main

import "fmt"

type Node struct {
    Value int
    Left  *Node
    Right *Node
}

func (n *Node) InOrder() func(func(int) bool) bool {
    return func(yield func(int) bool) bool {
        var walk func(*Node) bool
        walk = func(node *Node) bool {
            if node == nil {
                return true
            }
            if !walk(node.Left) {
                return false
            }
            if !yield(node.Value) {
                return false
            }
            return walk(node.Right)
        }
        return walk(n)
    }
}

func main() {
    root := &Node{Value: 10,
        Left:  &Node{Value: 5, Left: &Node{Value: 2}, Right: &Node{Value: 7}},
        Right: &Node{Value: 15, Right: &Node{Value: 20}},
    }
    for v := range root.InOrder() {
        fmt.Println(v)
    }
}

Este exemplo mostra como um iterador pode encapsular uma recursão complexa, tornando o código de iteração limpo e direto.

Exemplo 3: Usando iteradores com slices e maps

A biblioteca padrão do Go 1.23 inclui funções utilitárias para trabalhar com iteradores. Por exemplo, slices.Collect pode converter um iterador em um slice.

package main

import (
    "fmt"
    "slices"
)

func squares(n int) func(func(int) bool) bool {
    return func(yield func(int) bool) bool {
        for i := 1; i <= n; i++ {
            if !yield(i * i) {
                return false
            }
        }
        return true
    }
}

func main() {
    sq := slices.Collect(squares(5))
    fmt.Println(sq) // [1 4 9 16 25]
}

Da mesma forma, maps.Collect pode converter um iterador de pares em um mapa.

Boas práticas e observações finais

Ao criar iteradores, é importante seguir o padrão de retorno bool para indicar se a iteração foi interrompida. Isso permite que a função iteradora pare de gerar valores se o consumidor decidir encerrar o loop (por exemplo, com um break). Além disso, evite efeitos colaterais desnecessários dentro do iterador; ele deve ser uma função pura sempre que possível. Os iteradores são particularmente úteis para criar APIs expressivas e eficientes, mas lembre-se de que, para coleções pequenas, um slice simples pode ser mais direto.

Referências

Exercícios

  1. Exercício 1: Crie um iterador que gere os primeiros N números da sequência de Fibonacci. Use range para imprimir os 10 primeiros.

    ✓ Resposta:
    func fibonacci(n int) func(func(int) bool) bool {
        return func(yield func(int) bool) bool {
            a, b := 0, 1
            for i := 0; i < n; i++ {
                if !yield(a) {
                    return false
                }
                a, b = b, a+b
            }
            return true
        }
    }
    
    func main() {
        for v := range fibonacci(10) {
            fmt.Println(v)
        }
    }
  2. Exercício 2: Escreva um iterador que percorra uma lista encadeada simples. Defina a estrutura e a função iteradora.

    ✓ Resposta:
    type ListNode struct {
        Val  int
        Next *ListNode
    }
    
    func (l *ListNode) Iter() func(func(int) bool) bool {
        return func(yield func(int) bool) bool {
            for cur := l; cur != nil; cur = cur.Next {
                if !yield(cur.Val) {
                    return false
                }
            }
            return true
        }
    }
  3. Exercício 3: Use o iterador do exercício anterior para coletar os valores em um slice usando slices.Collect.

    ✓ Resposta:
    list := &ListNode{1, &ListNode{2, &ListNode{3, nil}}}
    vals := slices.Collect(list.Iter())
    fmt.Println(vals) // [1 2 3]
  4. Exercício 4: Crie um iterador que percorra os caracteres de uma string (como runas) e use range para imprimir cada runa e seu índice.

    ✓ Resposta:
    func runeIter(s string) func(func(int, rune) bool) bool {
        return func(yield func(int, rune) bool) bool {
            for i, r := range s {
                if !yield(i, r) {
                    return false
                }
            }
            return true
        }
    }
    
    func main() {
        for i, r := range runeIter("olá") {
            fmt.Printf("%d: %c\n", i, r)
        }
    }
  5. Exercício 5: Implemente um iterador que filtre números primos de um intervalo [2, N] e use-o para imprimir os primos até 20.

    ✓ Resposta:
    func isPrime(n int) bool {
        if n < 2 { return false }
        for i := 2; i*i <= n; i++ {
            if n%i == 0 { return false }
        }
        return true
    }
    
    func primesUpTo(limit int) func(func(int) bool) bool {
        return func(yield func(int) bool) bool {
            for i := 2; i <= limit; i++ {
                if isPrime(i) {
                    if !yield(i) {
                        return false
                    }
                }
            }
            return true
        }
    }
    
    func main() {
        for p := range primesUpTo(20) {
            fmt.Println(p)
        }
    }