Iteradores (range over func)
Nesta aula, exploramos a nova funcionalidade de iteradores em Go, introduzida na versão 1.23, que permite usar a sintaxe range sobre funções personalizadas. Aprendemos a criar funções iteradoras seguindo o padrão func(yield func(K,V) bool), a usá-las com range e a aplicá-las em casos práticos como iteração sobre estruturas de dados personalizadas, geração de sequências e processamento de dados sob demanda.
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) boolpara iterar sobre valores de tipo V, e
func(yield func(K, V) bool) boolpara 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,filterereducepodem 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.Collectemaps.Collectpodem 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
- Go Blog: Range Over Function Types
- Go Specification: Range Clause
- Package iter (Go 1.23)
- Package slices: Collect
- Package maps: Collect
- Go 1.23 Release Notes
- Proposal: Range over func
Exercícios
- Exercício 1: Crie um iterador que gere os primeiros N números da sequência de Fibonacci. Use
rangepara 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) } } - 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 } } - 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] - Exercício 4: Crie um iterador que percorra os caracteres de uma string (como runas) e use
rangepara 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) } } - 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) } }