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 usarbreaknemreturnde 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 orows.Next()do dia 96 ou obufio.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
FiltrareMapearprontos. 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 chamarsort.Strings. Agora é uma linha;slices.Alleslices.Values: transformam um slice num iterator (com e sem o índice). Úteis para passar um slice para funções que recebemiter.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.Linesestrings.SplitSeq(Go 1.24): equivalentes aostrings.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:
stringou[]byte: o conteúdo de um arquivo. É o caso doVERSION;embed.FS: um sistema de arquivos somente leitura com um ou vários arquivos e pastas. Aceita padrões comotemplates/*.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.FileServerFSserve os arquivos estáticos. Os arquivos ficam noembed.FScom o caminho completo (static/estilo.css), por isso registramos a rota como/static/;http.ServeFileFSserve um arquivo específico. OGET /{$}é a sintaxe de rotas do Go 1.22 para “somente a raiz”, sem pegar todos os outros caminhos;template.ParseFScarrega os templates;goose.SetBaseFS: o Goose do dia 87 também aceita umfs.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
embedprecisa ser importado. Se você usar sóstringou[]byte, sem o tipoembed.FS, o import fica comoimport _ "embed"; - Os caminhos são relativos ao arquivo
.goe 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 prefixoall:, 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 oembed.FSem produção. Como os dois sãofs.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.