Iteradores (range over func)
Nesta aula, exploramos a nova funcionalidade de iteradores em Go, introduzida na versão 1.23. Aprendemos como usar a cláusula range sobre funções que implementam o protocolo de iteração, os casos de uso mais comuns e vimos exemplos práticos de implementação.
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
yielde interrompa a iteração se ele retornarfalse, 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
Exercício 1: Crie um iterador que gere os números da sequência de Fibonacci até um valor máximo (ex.: 100). Use
rangepara 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) } }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) // }Exercício 3: Crie um iterador que produza pares (índice, valor) de uma slice. Use
rangepara 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) // }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 }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) }