Desde o Go 1.23, a linguagem ganhou uma forma oficial de escrever iteradores: a capacidade de usar for...range diretamente sobre uma função. O resumo do Go 1.23 menciona a novidade em uma linha; este guia aprofunda como ela funciona de verdade e quando vale a pena usá-la em produção. Se você já domina generics e concorrência com channels, vai reconhecer os dois pilares dos iteradores: type parameters e o padrão de “empurrar” valores para o chamador.
A motivação é prática. Antes do Go 1.23, percorrer uma estrutura customizada exigia expor um slice interno, implementar Len()/Get(i) ou abrir um channel só para iterar — e channels arrastam goroutines e custo. Com range-over-func, você escreve uma função que produz valores sob demanda, e o range cuida do resto. O resultado é código mais simples, pipelines preguiçosos (lazy) sem alocações intermediárias e composição natural: filtrar, mapear e acumular viram funções de uma linha.
Como funciona o range-over-func
A regra é direta: quando o range recebe uma função, o Go chama essa função passando como argumento outra função, yield. Cada chamada yield(valor) entrega um valor ao corpo do for. Se o chamador executar break, yield retorna false e a função iteradora deve parar. Quando a função retorna, o loop termina.
func UmACinco(yield func(int) bool) {
for i := 1; i <= 5; i++ {
if !yield(i) {
return // chamador deu break
}
}
}
func main() {
for v := range UmACinco {
fmt.Println(v) // 1 2 3 4 5
}
}
Repare em dois detalhes que confundem iniciantes. Primeiro, yield retorna um bool: você precisa checar e retornar quando ele devolver false, senão continua processando depois do chamador ter saído. Segundo, a função iteradora não itera “sozinha”: o range é quem a executa, então todo o trabalho acontece de forma síncrona, na mesma goroutine, sem canais.
O pacote iter: Seq e Seq2
O pacote padrão iter existe só para nomear duas assinaturas, que servem como “contrato” entre produtores e consumidores de iteradores:
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
Seq[V] é a versão de um valor (equivalente a for v := range). Seq2[K, V] é a versão de par chave-valor (equivalente a for k, v := range), usada para mapas e para slices com índice. O exemplo acima fica idiomático assim:
import "iter"
func UmACinco() iter.Seq[int] {
return func(yield func(int) bool) {
for i := 1; i <= 5; i++ {
if !yield(i) {
return
}
}
}
}
A função externa retorna a função interna. Isso permite criar iteradores parametrizados (Fibonacci, linhas de um arquivo, registros de um cursor) sem estado global.
Iterador infinito, consumo controlado
O caso-clássico que mostra a vantagem dos iteradores é gerar uma sequência teoricamente infinita e consumi-la com break. Fibonacci:
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
}
}
}
func main() {
for n := range Fibonacci() {
if n > 100 {
break
}
fmt.Println(n) // 0 1 1 2 3 5 8 13 21 34 55 89
}
}
Sem iteradores, isso exigiria um canal com goroutine (e o cuidado de fechá-lo) ou um loop manual com variável de estado. Aqui, break aciona o retorno false do yield, a função para e nada vaza. É o mesmo princípio de laziness que torna streams e pipelines eficientes em outras linguagens.
Coleções nativas: slices e maps
Você raramente vai escrever iter.Seq do zero. O Go já empacota iteradores para as coleções do dia a dia no pacote slices e maps:
import (
"maps"
"slices"
)
func main() {
nomes := []string{"Ana", "Bruno", "Carla"}
// só valores
for n := range slices.Values(nomes) {
fmt.Println(n)
}
// índice + valor (substitui o range clássico quando conveniente)
for i, n := range slices.All(nomes) {
fmt.Println(i, n)
}
// de trás pra frente
for i, n := range slices.Backward(nomes) {
fmt.Println(i, n)
}
// mapas com ordem indefinida, mas composável
idades := map[string]int{"Ana": 30, "Bruno": 25}
for k, v := range maps.All(idades) {
fmt.Println(k, v)
}
}
A mágica está em funções de coleta que consomem um iterador e devolvem uma estrutura pronta: slices.Collect junta tudo num slice, slices.Sorted devolve ordenado, slices.AppendSeq acumula num slice existente, e maps.Collect / maps.Insert populam mapas:
pares := slices.Collect(func(yield func(int) bool) {
for i := 2; i <= 10; i += 2 {
if !yield(i) {
return
}
}
})
// pares == []int{2, 4, 6, 8, 10}
Pipelines preguiçosos: filtro e mapa
É aqui que iteradores brilham. Combinando slices.Values, um Filter e um Map, você processa coleções grandes sem alocar slices intermediárias — cada elemento percorre o pipeline de ponta a ponta antes do próximo ser lido:
func Filter[V any](seq iter.Seq[V], pred func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if pred(v) && !yield(v) {
return
}
}
}
}
func Map[V, W any](seq iter.Seq[V], f func(V) W) iter.Seq[W] {
return func(yield func(W) bool) {
for v := range seq {
if !yield(f(v)) {
return
}
}
}
}
Encadeando:
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
paresDobrados := slices.Collect(
Map(
Filter(slices.Values(nums), func(n int) bool { return n%2 == 0 }),
func(n int) int { return n * 2 },
),
)
// paresDobrados == []int{4, 8, 12, 16, 20}
Sem iteradores, isso seriam dois slices temporários (um do filtro, um do mapa). Com iteradores, o processamento é elemento a elemento e pode ser zero-alocação se as closures forem simples. Para coleções de centenas de milhares de itens, a diferença de memória é grande. Se for medir isso de verdade, leia o guia de pprof em produção.
Processamento em lotes com slices.Chunk
Outro iterador nativo que resolve um problema recorrente é o slices.Chunk, que fatia uma coleção em pedaços de tamanho fixo — útil para batch insert, paginação em memória e chamadas em lote:
func ProcessarTudo(ids []int64) {
for lote := range slices.Chunk(ids, 500) {
// lote é um []int64 com até 500 elementos
if err := repositorio.BulkInsert(lote); err != nil {
return err
}
}
}
Antes, todo mundo reescrevia essa mesma lógica de “calcula início e fim, fatia com cuidado no limite”. Agora é uma linha legível.
Pull iterators: iter.Pull e iter.Pull2
Tudo até aqui usa push iterators: a função “empurra” valores para o yield. Existe o caminho inverso, o pull iterator, em que o chamador “puxa” um valor de cada vez com uma função next(). O pacote iter oferece iter.Pull e iter.Pull2:
func Zip[A, B any](a iter.Seq[A], b iter.Seq[B]) iter.Seq2[A, B] {
return func(yield func(A, B) bool) {
nextA, stopA := iter.Pull(a)
defer stopA()
nextB, stopB := iter.Pull(b)
defer stopB()
for {
va, okA := nextA()
vb, okB := nextB()
if !okA || !okB {
return
}
if !yield(va, vb) {
return
}
}
}
}
Zip é o exemplo canônico: você só consegue intercalar duas sequências puxando um valor de cada uma. Note o defer stop() — obrigatório. O pull iterator usa uma coroutine interna e, se você não chamar stop, vaza. Para o dia a dia, prefira push iterators; reserve iter.Pull para casos de intercalação real.
Armadilhas que custam caro
Ignorar o retorno de yield. Sempre faça if !yield(v) { return }. Se você continuar iterando depois de um break, pode processar milhões de itens à toa — e, em iteradores sobre I/O (linhas de arquivo, registros), isso significa ler dados que ninguém vai usar.
Chamar yield depois de a função ter retornado. Guardar a função yield em uma struct e invocá-la mais tarde (de outra goroutine, de um timer) entra em pânico. O contrato é claro: yield só é válida durante a execução síncrona do iterador.
Esquecer stop() no pull iterator. Vaza coroutine. Sempre use defer stop() logo após criar o pull iterator.
Assumir ordem em maps.All. Mapas em Go não têm ordem garantida; maps.All reflete isso. Se precisa de ordem, extraia as chaves com slices.Sorted(maps.Keys(m)) e itere sobre elas.
Iteradores, channels e callbacks: qual usar?
A escolha depende de onde os dados nascem:
| Origem dos dados | Ferramenta certa |
|---|---|
| Coleção em memória (slice, mapa, árvore) | Iterador (iter.Seq) |
| Pipeline de transformações síncronas | Iterador (lazy, zero alocação) |
| Eventos assíncronos, streaming de rede | Channel ou callback |
| Trabalho concorrente com erro agregado | errgroup |
| Producer/consumer entre goroutines | Channel (veja channels em produção) |
A regra prática: se tudo acontece na mesma goroutine e os dados já estão prontos (ou podem ser gerados sob demanda síncrona), iterador. Se os dados chegam no tempo deles, de outra goroutine, channel. Iteradores não substituem channels — eles substituem o pattern feio de “expõe um slice interno ou implementa três métodos só pra dar um range”.
Testando iteradores
Iteradores são funções comuns, então testes de tabela funcionam direto. O truque é coletar a saída com slices.Collect e comparar:
func TestFibonacci(t *testing.T) {
tests := []struct {
nome string
limite int
quer []int
}{
{"ate 10", 10, []int{0, 1, 1, 2, 3, 5, 8}},
{"zero", 0, []int{}},
}
for _, tt := range tests {
t.Run(tt.nome, func(t *testing.T) {
got := []int{}
for n := range Fibonacci() {
if n > tt.limite {
break
}
got = append(got, n)
}
if !slices.Equal(got, tt.quer) {
t.Errorf("got %v, quer %v", got, tt.quer)
}
})
}
}
Teste também o caminho de break (iterador infinito com limite) e o caso vazio — são os dois lugares onde iteradores costumam quebrar.
Perguntas frequentes
Preco do Go 1.23 em diante para usar iteradores?
Sim, range-over-func e o pacote iter são estáveis desde o Go 1.23 (agosto de 2024). Qualquer projeto em Go 1.23+ já tem o recurso disponível na biblioteca padrão, sem dependências externas.
Iteradores deixam o código mais lento?
Não — pelo contrário, costumam ser mais rápidos e alocam menos que abordagens baseadas em slice temporário ou channel, porque o compilador inlina a função yield e não há goroutine envolvida nos push iterators. Como sempre, meça com go test -bench antes de otimizar; não troque um for simples por um iterador só por estética.
Posso converter um iterador de volta em slice ou mapa?
Pode. Use slices.Collect(seq) para um slice, slices.Sorted(seq) para um slice ordenado, slices.AppendSeq(dst, seq) para acumular e maps.Collect(seq2) ou maps.Insert(m, seq2) para montar um mapa. É assim que você “materializa” um pipeline preguiçoso quando precisa do resultado final.
Como interrompo um iterador infinito?
Com break no for, exatamente como em um loop comum. O break faz o yield retornar false, e a função iteradora é responsável por checar esse retorno e parar. Por isso a regra de sempre escrever if !yield(v) { return }.
Existe iterador reverso?
Sim. slices.Backward(s) retorna um iter.Seq2[int, T] que percorre do fim para o início. Para estruturas customizadas, você escreve a ordem que quiser dentro da função iteradora — ela só precisa chamar yield na sequência desejada.
Iteradores não são açúcar sintático: são a forma idiomática, desde o Go 1.23, de expor sequências lazy e composáveis em Go sem recorrer a channels ou slices intermediários. Comece substituindo aquele método Items() []T que devolvia uma cópia por um iter.Seq[T], adote slices.Values/maps.All no lugar de range direto quando precisar compor, e guarde iter.Pull só para intercalar sequências. Para se aprofundar na base teórica, leia o guia de generics, que explica os type parameters que tornam Seq[V] possível, e o guia de concorrência para entender a fronteira exata entre iterador síncrono e channel concorrente.