Bem-vindo à aula 48 do nosso curso de Go! Hoje vamos mergulhar em um dos recursos mais aguardados da linguagem: os iteradores, também conhecidos como range over func. Essa funcionalidade chegou oficialmente na versão 1.23 do Go e muda a forma como podemos percorrer coleções e gerar sequências de valores.

Antes dessa novidade, o range era restrito a tipos nativos como slices, arrays, mapas e strings. Agora, podemos criar funções que podem ser usadas diretamente com range, abrindo um leque de possibilidades para escrever código mais limpo, expressivo e eficiente.

Ao longo desta aula, vamos entender o que são iteradores, como implementá-los e como usá-los no seu dia a dia. Prepare-se para explorar desde os conceitos básicos até exemplos práticos que você pode aplicar imediatamente nos seus projetos.

A novidade de iteradores

Antes do Go 1.23, a iteração com range era limitada a estruturas de dados nativas: slices, arrays, mapas, strings e canais. Isso significava que, para iterar sobre uma estrutura personalizada, era preciso implementar manualmente métodos como Next() ou criar funções que retornassem slices, o que muitas vezes resultava em código verboso e pouco flexível.

Com a introdução dos iteradores, a linguagem oferece um protocolo padrão para iteração, permitindo que qualquer função que siga esse protocolo seja utilizada diretamente com range. Isso unifica a forma de iterar e permite que bibliotecas e aplicações exponham coleções de forma mais natural.

Um iterador em Go é simplesmente uma função que recebe uma função de callback (chamada de yield) e a invoca para cada valor da sequência. Quando a iteração deve parar, a função retorna. Existem dois tipos principais: iteradores que produzem apenas um valor por vez e iteradores que produzem pares de valores (como chave/valor).

A grande vantagem é que o compilador otimiza o uso desses iteradores, muitas vezes eliminando a sobrecarga de chamadas de função, tornando o código tão eficiente quanto uma iteração manual.

range sobre funções

Agora vamos ver como usar range com funções. A sintaxe é a mesma que você já conhece, mas o alvo do range pode ser uma função que implementa o protocolo de iteração.

Para uma função que produz um único valor por iteração, a assinatura deve ser:

func(yield func(V) bool)

Para uma função que produz dois valores (por exemplo, chave e valor), a assinatura é:

func(yield func(K, V) bool)

A função yield retorna um booleano: se retornar true, a iteração continua; se retornar false, a iteração é interrompida. Isso permite que o consumidor controle o fluxo, por exemplo, para parar cedo em uma busca.

Veja um exemplo simples de um iterador que percorre números de 1 a N:

package main

import "fmt"

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

func main() {
    for i := range Count(5) {
        fmt.Println(i)
    }
}

Ao executar, você verá os números de 1 a 5 impressos. Note como a função Count retorna uma função que é usada diretamente no range.

Casos de uso

Os iteradores são extremamente versáteis e podem ser usados em muitos cenários. Aqui estão alguns casos de uso comuns:

  • Sequências infinitas ou preguiçosas: você pode criar iteradores que geram valores sob demanda, como números de Fibonacci, sem precisar alocar uma slice enorme.
  • Árvores e estruturas de dados recursivas: iterar sobre uma árvore binária, por exemplo, pode ser feito de forma elegante com um iterador que percorre os nós em ordem.
  • Leitura de arquivos linha por linha: em vez de carregar o arquivo inteiro na memória, você pode usar um iterador que lê e entrega cada linha sob demanda.
  • Transformações e filtros: você pode criar iteradores que filtram ou transformam valores de uma fonte, compondo operações de forma funcional.
  • Canais e concorrência: iteradores podem encapsular lógica de canais, permitindo que você itere sobre valores recebidos de forma síncrona.

Além disso, a biblioteca padrão já está adotando iteradores em pacotes como slices e maps, facilitando operações comuns.

Exemplos

Vamos aprofundar com alguns exemplos práticos. Primeiro, um iterador que gera números pares até um limite:

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

Para usá-lo:

for n := range EvenNumbers(10) {
    fmt.Println(n)
}

Outro exemplo: um iterador que percorre uma árvore binária em ordem. Suponha a seguinte estrutura:

type TreeNode struct {
    Val   int
    Left  *TreeNode
    Right *TreeNode
}

Podemos criar um iterador que percorre os nós em ordem (esquerda, raiz, direita):

func InOrder(root *TreeNode) func(yield func(*TreeNode) bool) {
    return func(yield func(*TreeNode) bool) {
        var walk func(*TreeNode) bool
        walk = func(node *TreeNode) bool {
            if node == nil {
                return true
            }
            if !walk(node.Left) {
                return false
            }
            if !yield(node) {
                return false
            }
            return walk(node.Right)
        }
        walk(root)
    }
}

E usamos assim:

for node := range InOrder(root) {
    fmt.Println(node.Val)
}

Por fim, um exemplo de iterador com dois valores (chave/valor) para um mapa personalizado:

func MapEntries(m map[string]int) func(yield func(string, int) bool) {
    return func(yield func(string, int) bool) {
        for k, v := range m {
            if !yield(k, v) {
                return
            }
        }
    }
}

Uso:

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

Note que a ordem de iteração de um mapa não é garantida, mas o iterador encapsula essa lógica.

Boas práticas e observações finais

Ao criar iteradores, é importante seguir algumas boas práticas:

  • Respeite o contrato do yield: sempre verifique o retorno de yield e interrompa a iteração se ele retornar false, para evitar trabalho desnecessário.
  • Evite efeitos colaterais: idealmente, um iterador deve ser puro, ou seja, não modificar o estado externo, a menos que seja explicitamente necessário.
  • Documente o comportamento: se o iterador pode ser infinito, deixe claro na documentação para que o consumidor saiba que deve interromper a iteração manualmente.
  • Combine iteradores: você pode compor iteradores para criar pipelines de processamento de dados, como filtros e transformações.

Lembre-se de que os iteradores são uma adição recente, então verifique se a versão do Go utilizada é 1.23 ou superior. Para projetos que precisam de compatibilidade com versões anteriores, essa funcionalidade não está disponível.

Com esse conhecimento, você está pronto para usar iteradores em seus projetos e escrever código mais expressivo e eficiente. Experimente criar seus próprios iteradores para diferentes estruturas de dados!

Exercícios

  1. Exercício 1: Crie um iterador que gere os números da sequência de Fibonacci até um valor máximo (ex.: 100). Use range para imprimir cada número.

    ✓ Resposta:
    func Fibonacci(max int) func(yield func(int) bool) {
        return func(yield func(int) bool) {
            a, b := 0, 1
            for a <= max {
                if !yield(a) {
                    return
                }
                a, b = b, a+b
            }
        }
    }
    
    func main() {
        for n := range Fibonacci(100) {
            fmt.Println(n)
        }
    }
  2. Exercício 2: Implemente um iterador que percorra as linhas de um arquivo de texto (use bufio.Scanner). Teste com um arquivo de exemplo.

    ✓ Resposta:
    func ReadLines(filename string) func(yield func(string) bool) error {
        return func(yield func(string) bool) error {
            f, err := os.Open(filename)
            if err != nil {
                return err
            }
            defer f.Close()
            scanner := bufio.NewScanner(f)
            for scanner.Scan() {
                if !yield(scanner.Text()) {
                    return nil
                }
            }
            return scanner.Err()
        }
    }
    
    // Uso:
    // for line := range ReadLines("file.txt") {
    //     fmt.Println(line)
    // }
  3. Exercício 3: Crie um iterador que produza pares (índice, valor) de uma slice. Use range para percorrer e imprimir.

    ✓ Resposta:
    func SliceIndices[T any](s []T) func(yield func(int, T) bool) {
        return func(yield func(int, T) bool) {
            for i, v := range s {
                if !yield(i, v) {
                    return
                }
            }
        }
    }
    
    // Uso:
    // for i, v := range SliceIndices([]int{10,20,30}) {
    //     fmt.Println(i, v)
    // }
  4. Exercício 4: Escreva um iterador que filtre números primos de uma slice. Use um iterador que receba a slice e uma função de predicado, mas para simplificar, crie um iterador específico para primos.

    ✓ Resposta:
    func Primes(numbers []int) func(yield func(int) bool) {
        return func(yield func(int) bool) {
            for _, n := range numbers {
                if isPrime(n) {
                    if !yield(n) {
                        return
                    }
                }
            }
        }
    }
    
    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
    }
  5. Exercício 5: Usando iteradores, implemente uma função que retorne a soma de todos os números pares de 1 a 100, mas pare a iteração quando a soma ultrapassar 300. Use o iterador de pares da aula.

    ✓ Resposta:
    func main() {
        sum := 0
        for n := range EvenNumbers(100) {
            sum += n
            if sum > 300 {
                break
            }
        }
        fmt.Println(sum)
    }

Referências