Pular para o conteúdo principal

victorstein.dev

99/100 Dias de Golang - Iterators e embed

Table of Contents

# Iterators e embed

No penúltimo post da série, dois recursos da linguagem que não têm muito a ver um com o outro, mas que aparecem cada vez mais em código Go moderno: os iterators, que chegaram no Go 1.23, e o embed, que permite colocar arquivos dentro do binário.

# Iterators

## O problema

O for range sempre funcionou com os tipos da linguagem: slices, arrays, maps, strings e channels. Desde o Go 1.22, funciona até com inteiros (for i := range 10). Mas e se criarmos a nossa própria coleção, como uma lista encadeada ou uma árvore? Até pouco tempo, as opções eram:

  • Retornar um slice com todos os elementos: simples, mas aloca tudo de uma vez, mesmo que quem chamou só queira o primeiro;
  • Receber um callback: lista.ParaCada(func(v T) { ... }). Funciona, mas não dá para usar break nem return de dentro do callback;
  • Usar um channel e uma goroutine: dá para fazer for v := range canal, mas é lento, e se quem consome parar no meio, a goroutine fica presa para sempre;
  • Um tipo com Next(): como o rows.Next() do dia 96 ou o bufio.Scanner. Cada biblioteca fazia do seu jeito.

Desde o Go 1.23, o for range também aceita funções com um formato específico. Isso é o range-over-func, e essas funções são os iterators.

## iter.Seq

Um iterator é uma função que recebe outra função, chamada por convenção de yield, e chama o yield para cada valor que quer entregar. O pacote iter define os tipos:

type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)

Um exemplo:

package main

import (
	"fmt"
	"iter"
)

func Contagem(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; i <= n; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

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 Contagem(3) {
		fmt.Println("contagem:", n)
	}

	for n := range Fibonacci() {
		if n > 50 {
			break
		}
		fmt.Print(n, " ")
	}
	fmt.Println()
}

Saída:

contagem: 1
contagem: 2
contagem: 3
0 1 1 2 3 5 8 13 21 34

O que acontece por baixo dos panos: o compilador transforma o corpo do for numa função e passa ela como yield. Cada vez que o iterator chama yield(i), o corpo do loop roda com aquele valor.

O bool que o yield retorna diz se o loop quer continuar. Quando o corpo do for executa um break (ou um return), o yield retorna false, e o iterator precisa parar. É isso que permite o Fibonacci ser infinito: ele gera números para sempre, e quem decide quando parar é o break no loop. Os valores também são gerados sob demanda (lazy): o número 55 nunca chegou a ser usado.

E se o iterator ignorar o retorno do yield?

func Ruim() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; i <= 3; i++ {
			yield(i) // ignorando o retorno
		}
	}
}
1
panic: runtime error: range function continued iteration after function for loop body returned false

O runtime detecta e gera um panic. Então a regra é: sempre verifique o retorno do yield.

## iter.Seq2 e iterators em tipos próprios

O iter.Seq2 entrega dois valores por vez, como o for i, v := range slice. A convenção é que as coleções tenham um método All() que retorna um iterator:

package main

import (
	"fmt"
	"iter"
)

type no[T any] struct {
	valor   T
	proximo *no[T]
}

type Lista[T any] struct {
	inicio *no[T]
	fim    *no[T]
}

func (l *Lista[T]) Adicionar(v T) {
	n := &no[T]{valor: v}
	if l.fim == nil {
		l.inicio, l.fim = n, n
		return
	}
	l.fim.proximo = n
	l.fim = n
}

// All percorre a lista retornando a posição e o valor de cada elemento
func (l *Lista[T]) All() iter.Seq2[int, T] {
	return func(yield func(int, T) bool) {
		i := 0
		for n := l.inicio; n != nil; n = n.proximo {
			if !yield(i, n.valor) {
				return
			}
			i++
		}
	}
}

func main() {
	var tarefas Lista[string]
	tarefas.Adicionar("estudar iterators")
	tarefas.Adicionar("escrever o post 99")
	tarefas.Adicionar("revisar o post")

	for i, t := range tarefas.All() {
		fmt.Printf("%d: %s\n", i, t)
	}
}

Saída:

0: estudar iterators
1: escrever o post 99
2: revisar o post

A nossa lista encadeada genérica (generics, dia 13) agora funciona no for range como qualquer slice. E quem usa a lista não precisa saber nada sobre nós e ponteiros.

## Compondo iterators

Como um iterator é só uma função, dá para escrever funções que recebem um iterator e retornam outro. Três adaptadores clássicos:

func Filtrar[T any](seq iter.Seq[T], manter func(T) bool) iter.Seq[T] {
	return func(yield func(T) bool) {
		for v := range seq {
			if manter(v) && !yield(v) {
				return
			}
		}
	}
}

func Mapear[T, R any](seq iter.Seq[T], f func(T) R) iter.Seq[R] {
	return func(yield func(R) bool) {
		for v := range seq {
			if !yield(f(v)) {
				return
			}
		}
	}
}

func Limitar[T any](seq iter.Seq[T], n int) iter.Seq[T] {
	return func(yield func(T) bool) {
		if n <= 0 {
			return
		}
		i := 0
		for v := range seq {
			if !yield(v) {
				return
			}
			i++
			if i == n {
				return
			}
		}
	}
}

Juntando com o Fibonacci infinito:

func main() {
	pares := Filtrar(Fibonacci(), func(n int) bool { return n%2 == 0 })
	textos := Mapear(pares, func(n int) string { return fmt.Sprintf("<%d>", n) })
	primeiros := Limitar(textos, 6)

	fmt.Println(slices.Collect(primeiros))
}
[<0> <2> <8> <34> <144> <610>]

Os seis primeiros números pares de Fibonacci, formatados como texto. Mesmo partindo de uma sequência infinita, o programa termina: nada é calculado até o slices.Collect começar a pedir valores, e quando o Limitar chega em 6, ele para e o “pare” vai se propagando para trás até o Fibonacci. Nenhum slice intermediário foi criado.

A biblioteca padrão não tem Filtrar e Mapear prontos. Na biblioteca lo e em outras parecidas existem versões, mas o Go incentiva escrever um loop simples na maioria dos casos. Os adaptadores valem a pena quando a mesma transformação aparece em vários lugares.

## Iterators na biblioteca padrão

Junto com o pacote iter, os pacotes slices e maps ganharam funções que produzem e consomem iterators. E no Go 1.24 vieram mais alguns no strings e no bytes:

package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

func main() {
	estoque := map[string]int{"teclado": 12, "mouse": 30, "monitor": 5, "cabo": 100}

	// as chaves de um map em ordem
	for _, produto := range slices.Sorted(maps.Keys(estoque)) {
		fmt.Printf("%-8s %3d\n", produto, estoque[produto])
	}

	nomes := []string{"ana", "bruno", "carla"}
	for i, nome := range slices.All(nomes) {
		fmt.Println(i, nome)
	}

	// do iterator de volta para um slice
	maiusculos := slices.Collect(func(yield func(string) bool) {
		for _, n := range nomes {
			if !yield(strings.ToUpper(n)) {
				return
			}
		}
	})
	fmt.Println(maiusculos)

	csv := "id,nome,email\n1,Victor,victor@exemplo.com\n2,Ana,ana@exemplo.com\n"
	for linha := range strings.Lines(csv) {
		campos := slices.Collect(strings.SplitSeq(strings.TrimSpace(linha), ","))
		fmt.Printf("%d campos: %v\n", len(campos), campos)
	}
}

Saída:

cabo     100
monitor    5
mouse     30
teclado   12
0 ana
1 bruno
2 carla
[ANA BRUNO CARLA]
3 campos: [id nome email]
3 campos: [1 Victor victor@exemplo.com]
3 campos: [2 Ana ana@exemplo.com]
  • maps.Keys + slices.Sorted: percorrer um map em ordem. A ordem de um map em Go é aleatória (vimos no dia 4); antes era preciso copiar as chaves para um slice e chamar sort.Strings. Agora é uma linha;
  • slices.All e slices.Values: transformam um slice num iterator (com e sem o índice). Úteis para passar um slice para funções que recebem iter.Seq;
  • slices.Collect: o caminho inverso, junta os valores de um iterator num slice. Repare que passamos uma função anônima direto: qualquer função com o formato certo é um iterator;
  • strings.Lines e strings.SplitSeq (Go 1.24): equivalentes ao strings.Split, mas sem criar o slice com todas as partes. Para textos grandes, isso economiza memória.

## Um caso real: paginação

Um uso bem prático é esconder paginação. Muitas APIs retornam os dados em páginas, e o código que consome precisa controlar a página atual, saber se tem mais… Com um iterator, quem usa só vê uma sequência de usuários:

package main

import (
	"errors"
	"fmt"
	"iter"
)

type Usuario struct {
	ID   int
	Nome string
}

// buscarPagina simula uma API que retorna os usuários em páginas de 3
func buscarPagina(pagina int) ([]Usuario, bool, error) {
	fmt.Printf("  [buscando página %d]\n", pagina)
	todos := []Usuario{
		{1, "Ana"}, {2, "Bruno"}, {3, "Carla"}, {4, "Daniel"},
		{5, "Eva"}, {6, "Fábio"}, {7, "Gabi"},
	}
	if pagina == 3 {
		return nil, false, errors.New("API fora do ar")
	}
	inicio := (pagina - 1) * 3
	if inicio >= len(todos) {
		return nil, false, nil
	}
	fim := min(inicio+3, len(todos))
	return todos[inicio:fim], fim < len(todos), nil
}

func TodosUsuarios() iter.Seq2[Usuario, error] {
	return func(yield func(Usuario, error) bool) {
		for pagina := 1; ; pagina++ {
			usuarios, temMais, err := buscarPagina(pagina)
			if err != nil {
				yield(Usuario{}, err)
				return
			}
			for _, u := range usuarios {
				if !yield(u, nil) {
					return
				}
			}
			if !temMais {
				return
			}
		}
	}
}

func main() {
	for u, err := range TodosUsuarios() {
		if err != nil {
			fmt.Println("erro:", err)
			break
		}
		fmt.Println(u.ID, u.Nome)
		if u.Nome == "Daniel" {
			fmt.Println("encontrei o Daniel, parando")
			break
		}
	}

	fmt.Println("---")

	for u, err := range TodosUsuarios() {
		if err != nil {
			fmt.Println("erro:", err)
			break
		}
		fmt.Println(u.ID, u.Nome)
	}
}

Saída:

  [buscando página 1]
1 Ana
2 Bruno
3 Carla
  [buscando página 2]
4 Daniel
encontrei o Daniel, parando
---
  [buscando página 1]
1 Ana
2 Bruno
3 Carla
  [buscando página 2]
4 Daniel
5 Eva
6 Fábio
  [buscando página 3]
erro: API fora do ar

No primeiro loop, assim que o Daniel é encontrado, o break faz o yield retornar false e a terceira página nunca é buscada. No segundo, percorremos tudo até a API falhar na página 3.

Para os erros, usamos um padrão comum: um iter.Seq2[Usuario, error], em que o segundo valor é o erro. Quem consome verifica o err a cada volta, como faria com o retorno de uma função comum.

## iter.Pull

Os iterators que vimos até aqui são do tipo push: o iterator controla o ritmo e “empurra” os valores para o corpo do loop. Às vezes precisamos do contrário, pedir o próximo valor quando quisermos, por exemplo, para percorrer dois iterators ao mesmo tempo. Para isso existe o iter.Pull:

package main

import (
	"fmt"
	"iter"
	"slices"
)

func main() {
	nomes := slices.Values([]string{"Ana", "Bruno", "Carla"})
	idades := slices.Values([]int{31, 25})

	proximoNome, pararNomes := iter.Pull(nomes)
	defer pararNomes()
	proximaIdade, pararIdades := iter.Pull(idades)
	defer pararIdades()

	for {
		nome, ok1 := proximoNome()
		idade, ok2 := proximaIdade()
		if !ok1 || !ok2 {
			break
		}
		fmt.Println(nome, idade)
	}
}
Ana 31
Bruno 25

O iter.Pull converte o iterator numa função proximo(), que retorna o valor e se ele existe, e numa função parar(), que deve ser chamada para liberar os recursos, por isso o defer. Como a lista de idades acabou antes, a Carla ficou de fora.

# embed

Agora o segundo assunto. Uma das vantagens do Go é gerar um único binário, que é só copiar para o servidor e rodar (o Dockerfile do dia 75 mostra isso). Mas aplicações reais costumam precisar de outros arquivos: HTML, CSS, templates, migrações SQL, arquivos de configuração… E aí o deploy volta a ser “o binário mais essas pastas, no lugar certo”.

O pacote embed, do Go 1.16, resolve isso colocando os arquivos dentro do binário na hora da compilação.

## Uma aplicação com tudo embutido

Vamos montar uma aplicação com esta estrutura:

site/
├── main.go
├── VERSION
├── migrations/
│   └── 00001_criar_pedidos.sql
├── static/
│   ├── estilo.css
│   └── index.html
└── templates/
    └── pedido.html

O templates/pedido.html é um template do pacote html/template:

<h1>Pedido #{{.ID}}</h1>
<p>Cliente: {{.Cliente}}</p>
<p>Total: R$ {{printf "%.2f" .Total}}</p>

E o main.go:

package main

import (
	"database/sql"
	"embed"
	"html/template"
	"io/fs"
	"log"
	"net/http"
	"os"
	"strings"

	_ "github.com/jackc/pgx/v5/stdlib"
	"github.com/pressly/goose/v3"
)

//go:embed VERSION
var versao string

//go:embed static
var arquivosEstaticos embed.FS

//go:embed templates/*.html
var arquivosTemplates embed.FS

//go:embed migrations/*.sql
var migracoes embed.FS

var templates = template.Must(template.ParseFS(arquivosTemplates, "templates/*.html"))

func migrar(dsn string) error {
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		return err
	}
	defer db.Close()

	goose.SetBaseFS(migracoes)
	if err := goose.SetDialect("postgres"); err != nil {
		return err
	}
	return goose.Up(db, "migrations")
}

func main() {
	if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
		if err := migrar(dsn); err != nil {
			log.Fatal(err)
		}
	}

	mux := http.NewServeMux()

	mux.Handle("GET /static/", http.FileServerFS(arquivosEstaticos))

	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		http.ServeFileFS(w, r, arquivosEstaticos, "static/index.html")
	})

	mux.HandleFunc("GET /pedido", func(w http.ResponseWriter, r *http.Request) {
		dados := struct {
			ID      int
			Cliente string
			Total   float64
		}{42, "Victor", 429.80}
		templates.ExecuteTemplate(w, "pedido.html", dados)
	})

	mux.HandleFunc("GET /versao", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte(strings.TrimSpace(versao) + "\n"))
	})

	entradas, _ := fs.ReadDir(arquivosEstaticos, "static")
	for _, e := range entradas {
		log.Println("arquivo embutido:", e.Name())
	}

	log.Printf("versão %s rodando em :8099", strings.TrimSpace(versao))
	log.Fatal(http.ListenAndServe(":8099", mux))
}

O //go:embed é uma diretiva para o compilador: um comentário especial, sem espaço entre o // e o go, logo acima de uma variável do pacote. Ele diz quais arquivos colocar naquela variável, e o tipo da variável define o formato:

  • string ou []byte: o conteúdo de um arquivo. É o caso do VERSION;
  • embed.FS: um sistema de arquivos somente leitura com um ou vários arquivos e pastas. Aceita padrões como templates/*.html.

O embed.FS implementa a interface fs.FS, a abstração de sistema de arquivos da biblioteca padrão. E várias partes da biblioteca padrão aceitam um fs.FS:

  • http.FileServerFS serve os arquivos estáticos. Os arquivos ficam no embed.FS com o caminho completo (static/estilo.css), por isso registramos a rota como /static/;
  • http.ServeFileFS serve um arquivo específico. O GET /{$} é a sintaxe de rotas do Go 1.22 para “somente a raiz”, sem pegar todos os outros caminhos;
  • template.ParseFS carrega os templates;
  • goose.SetBaseFS: o Goose do dia 87 também aceita um fs.FS, então as migrações vão dentro do binário. A aplicação pode aplicar as migrações sozinha ao iniciar.

Usei o goose v3.26.0 aqui:

go get github.com/pressly/goose/v3@v3.26.0 github.com/jackc/pgx/v5@v5.7.6

## Testando

Para provar que os arquivos estão mesmo dentro do binário, compilei, copiei só o executável para uma pasta vazia e rodei de lá, com um PostgreSQL no Docker:

go build -o site .
mkdir /tmp/teste && cp site /tmp/teste && cd /tmp/teste
ls
# site

DATABASE_URL="postgres://postgres:postgres@localhost:5432/loja" ./site
2025/12/16 10:00:00 OK   00001_criar_pedidos.sql (2.18ms)
2025/12/16 10:00:00 goose: successfully migrated database to version: 1
2025/12/16 10:00:00 arquivo embutido: estilo.css
2025/12/16 10:00:00 arquivo embutido: index.html
2025/12/16 10:00:00 versão 1.4.2 rodando em :8099

A migração rodou sem existir nenhum .sql na pasta. E as rotas:

$ curl localhost:8099/versao
1.4.2

$ curl localhost:8099/pedido
<h1>Pedido #42</h1>
<p>Cliente: Victor</p>
<p>Total: R$ 429.80</p>

$ curl localhost:8099/
<!doctype html>
<html>
<head><link rel="stylesheet" href="/static/estilo.css"></head>
<body><h1>Loja do Victor</h1></body>
</html>

$ curl localhost:8099/static/estilo.css
body { font-family: sans-serif; }

Tudo funcionando com um único arquivo. Num Dockerfile, a imagem final pode ter só o binário.

## Regras e cuidados

  • A variável precisa ser do pacote, não pode ser declarada dentro de uma função;
  • O pacote embed precisa ser importado. Se você usar só string ou []byte, sem o tipo embed.FS, o import fica como import _ "embed";
  • Os caminhos são relativos ao arquivo .go e não podem sair da pasta do módulo nem usar ..;
  • Arquivos que começam com . ou _ são ignorados quando você embute uma pasta inteira. Para incluí-los, use o prefixo all:, como em //go:embed all:static;
  • Os arquivos são lidos na compilação. Mudou um CSS? É preciso compilar de novo. Durante o desenvolvimento isso pode ser chato; uma solução é ler do disco com os.DirFS("static") em desenvolvimento e usar o embed.FS em produção. Como os dois são fs.FS, o resto do código não muda;
  • O tamanho dos arquivos vai para o binário. Embutir alguns megabytes de front-end tudo bem; embutir vídeos, provavelmente não.

# Referências

  1. Range Over Function Types
  2. Documentação iter
  3. Iterators in Go
  4. Documentação embed
  5. Documentação io/fs