Generics em Go deixam uma função servir para muitos tipos, com constraints dizendo quais tipos são permitidos. Iteradores deixam uma função entregar valores um de cada vez a um loop for, e parar no instante em que o loop dá break.
Generics e iteradores chegaram ao Go com alguns anos de diferença, mas cabem em um único post. Generics deixam você escrever uma função ou um tipo uma vez e usar com muitos tipos. Iteradores, construídos em cima deles, deixam um simples loop for ... range percorrer qualquer coisa que você consiga descrever com uma função.
Este post começa pelo problema que os generics resolvem, passa por constraints e tipos genéricos, e depois constrói iteradores do zero antes de usar os da biblioteca padrão. Todo programa abaixo rodou no Go 1.26, e a saída foi colada da execução.
O problema: a mesma função, duas vezes
Sem generics, uma função que soma números precisa escolher um único tipo numérico. Se você precisa dela para int e para float64, escreve duas vezes:
package main
import "fmt"
func SumInts(nums []int) int {
var total int
for _, n := range nums {
total += n
}
return total
}
func SumFloats(nums []float64) float64 {
var total float64
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(SumInts([]int{1, 2, 3}))
fmt.Println(SumFloats([]float64{1.5, 2.5}))
}
Ele imprime:
6
4
Os dois corpos são idênticos. Só o tipo mudou. Toda correção que você faz em um, precisa lembrar de fazer no outro, e um terceiro tipo numérico significa uma terceira cópia.
Uma função com um type parameter
Desde o Go 1.18, uma função pode receber um tipo como parâmetro, escrito entre colchetes antes dos parâmetros normais:
package main
import "fmt"
func Sum[T int | float64](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(Sum([]int{1, 2, 3}))
fmt.Println(Sum([]float64{1.5, 2.5}))
fmt.Println(Sum[float64]([]float64{0.25, 0.5}))
}
Ele imprime:
6
4
0.75
T é o type parameter. int | float64 é a constraint dele: o conjunto de tipos que T pode ser. Dentro da função, T funciona como qualquer outro nome de tipo, então var total T declara um zero value do tipo que T acabar sendo.
Nas duas primeiras chamadas, você não disse o que T era. O compilador olhou o argumento, viu um []int e deduziu que T só pode ser int. Isso é inferência de tipo. A terceira chamada dá o nome do tipo explicitamente, Sum[float64], o que se chama instanciação. Aqui ela é redundante, mas às vezes é a única opção.
Explicado como se você tivesse dez anos
Um cortador de biscoito faz a mesma forma em qualquer massa em que você o aperta: chocolate, simples, gengibre. Você não precisa de um cortador de estrela separado para cada tipo de massa.
Mas ele só funciona em massa. Aperte o cortador numa pedra e nada acontece. A constraint é a etiqueta no cortador que diz “funciona em massa”.
A versão precisa
Uma função genérica é um modelo que o compilador verifica uma vez, contra a constraint. Em cada chamada, o type argument é inferido dos argumentos comuns ou informado entre colchetes. O compilador então garante que esse tipo está no type set da constraint. Dentro do corpo, você só pode fazer o que todos os tipos do conjunto permitem: + funciona em T aqui porque tanto int quanto float64 aceitam essa operação.
Onde a analogia falha: um cortador de biscoito faz uma forma a partir de massa misturada, mas uma função genérica nunca mistura tipos em uma chamada. Sum em um []int devolve um int. Você não pode passar a ela um slice que guarda ints e floats ao mesmo tempo, porque Go não tem esse tipo de slice.
Quando a inferência não funciona
O compilador só infere um type parameter a partir dos argumentos que você passa. Se T aparece só no resultado, não há de onde inferir:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
x := Zero()
fmt.Println(x)
}
O build falha com:
./main.go:11:11: in call to Zero, cannot infer T
O Go não olha como você usa o resultado para adivinhar. Você precisa dar o nome do tipo:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
fmt.Printf("%d %q %v\n", Zero[int](), Zero[string](), Zero[bool]())
}
Ele imprime:
0 "" false
Cada instanciação dá o zero value do seu próprio tipo. Você vai encontrar a mesma situação com tipos genéricos, em que Stack[int]{} também não tem argumentos de onde inferir.
Constraints: any, comparable e cmp.Ordered
Uma constraint é uma interface, e ela decide quais operações o corpo da função pode usar. A mais ampla é any, que permite todos os tipos e, por isso, quase nenhuma operação. Você não consegue nem comparar dois valores any com ==, porque alguns tipos, como slices, não podem ser comparados:
package main
import "fmt"
func Equal[T any](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Equal(1, 1))
}
O build falha com:
./main.go:6:9: invalid operation: a == b (incomparable types in type set)
Duas constraints da biblioteca padrão cobrem os casos comuns. comparable permite todo tipo que aceita == e !=. cmp.Ordered, do pacote cmp adicionado no Go 1.21, permite todo tipo que aceita < e >: os inteiros, os floats e as strings.
package main
import (
"cmp"
"fmt"
)
func Index[T comparable](items []T, want T) int {
for i, item := range items {
if item == want {
return i
}
}
return -1
}
func Largest[T cmp.Ordered](items []T) T {
best := items[0]
for _, item := range items[1:] {
if item > best {
best = item
}
}
return best
}
func main() {
fmt.Println(Index([]string{"red", "green", "blue"}, "blue"))
fmt.Println(Index([]int{4, 8, 15}, 16))
fmt.Println(Largest([]int{4, 8, 15, 16}))
fmt.Println(Largest([]string{"pear", "apple", "fig"}))
}
Ele imprime:
2
-1
16
pear
Escolha a constraint mais estreita que deixa o corpo fazer o trabalho dele. Index só precisa de ==, então recebe comparable e funciona com structs também. Largest precisa de >, então recebe cmp.Ordered. A biblioteca padrão já tem as duas tarefas como slices.Index e slices.Max, com essas mesmas constraints.
Constraints de união e o que ~ significa
Você pode dar nome à sua própria constraint como uma interface que lista tipos, o que fica mais limpo do que escrever int | float64 em toda função. Há uma pegadinha com os tipos que você mesmo define. Este parece que deveria funcionar:
package main
import "fmt"
type Number interface {
int | float64
}
type Celsius float64
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
temps := []Celsius{21.5, 19, 23.5}
fmt.Println(Sum(temps))
}
O build falha com:
./main.go:21:17: Celsius does not satisfy Number (possibly missing ~ for float64 in Number)
Celsius é construído sobre float64, mas é um tipo diferente. Um tipo que você declara é distinto do tipo de que ele é feito, mesmo guardando os mesmos valores. O type set int | float64 contém exatamente dois tipos, e Celsius não é nenhum deles.
O compilador até diz como corrigir. Um ~ antes de um tipo quer dizer “este tipo, ou qualquer tipo cujo underlying type seja este”:
package main
import "fmt"
type Number interface {
~int | ~float64
}
type Celsius float64
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
temps := []Celsius{21.5, 19, 23.5}
total := Sum(temps)
fmt.Printf("%v %T\n", total, total)
}
Ele imprime:
64 main.Celsius
~float64 agora inclui float64, Celsius e qualquer outro tipo declarado como type X float64. O resultado continua sendo um Celsius, não um float64 simples, então você mantém o significado do seu tipo. É por isso que cmp.Ordered é escrito com til em todas as linhas: ele precisa aceitar o seu type UserID int além de int.
Tipos genéricos: um Stack[T]
Tipos também podem receber type parameters, assim como funções. Uma pilha é o caso clássico, porque a lógica é a mesma, seja lá o que ela guarde:
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
var nums Stack[int]
nums.Push(1)
nums.Push(2)
fmt.Println(nums.Pop())
fmt.Println(nums.Len())
words := Stack[string]{}
w, ok := words.Pop()
fmt.Printf("%q %v\n", w, ok)
words.Push("go")
fmt.Println(words.Pop())
}
Ele imprime:
2 true
1
"" false
go true
Stack[int] e Stack[string] são dois tipos separados. Cada método escreve Stack[T] no receiver, e T dentro do método é o tipo com que a pilha foi criada. Pop devolve um par comma-ok, o mesmo padrão que os maps usam, então uma pilha vazia dá 0 false ou "" false em vez de um panic.
Um método não pode declarar type parameters próprios, só usar os do tipo:
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Map[U any](f func(T) U) []U {
return nil
}
func main() {
fmt.Println(Stack[int]{})
}
O build falha com:
syntax error: method must have no type parameters
Quando você precisar disso, escreva uma função comum: func Map[T, U any](s *Stack[T], f func(T) U) []U.
Quando não usar generics
Generics servem para código que é idêntico entre tipos e só movimenta os valores: containers, Sum, Index, Largest. Quando o código chama métodos no valor, uma interface costuma ser mais simples.
Pegue uma função que imprime qualquer coisa com um método String. Você poderia escrever func Show[T fmt.Stringer](v T). Mas func Show(v fmt.Stringer) faz o mesmo trabalho, é mais fácil de ler e já era o jeito normal em Go muito antes de os generics existirem. O type parameter não acrescenta nada, porque o corpo só chama v.String().
Um guia aproximado:
- Se você escreveria o mesmo corpo para vários tipos e só o nome do tipo muda, use um type parameter.
- Se cada tipo faz algo próprio por trás de um método, use uma interface.
- Se hoje você tem um tipo só, use esse tipo. Dá para torná-lo genérico quando um segundo aparecer.
Iteradores: uma função que entrega valores
Desde o Go 1.23, um loop for ... range pode percorrer uma função. Uma função assim se chama iterador. Ela recebe um argumento, um callback que por convenção se chama yield, e chama esse callback uma vez para cada valor:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
for n := range Countdown(3) {
fmt.Println(n)
}
fmt.Println("liftoff")
}
Ele imprime:
3
2
1
liftoff
iter.Seq[int] é um tipo genérico do pacote iter. Ele é só um nome para func(yield func(int) bool). Countdown não conta nada sozinha. Ela devolve uma função que conta quando o loop pede.
Cada vez que o iterador chama yield(i), o corpo do loop roda uma vez com n valendo i. Quando a função do iterador retorna, o loop termina.
Explicado como se você tivesse dez anos
Um iterador é uma máquina de venda automática. Cada vez que você aperta o botão, ela entrega um item. Você não recebe o estoque inteiro despejado no chão de uma vez, e a máquina não precisa saber quantos você quer.
Você também pode ir embora quando quiser. A máquina percebe que ninguém está apertando o botão e para de entregar coisas.
A versão precisa
O corpo do loop vira a função yield. yield devolve true se o loop quer outro valor, e false se o loop terminou antes, por break, return ou um goto para fora do loop. O iterador precisa conferir esse bool e retornar assim que vir false. O range loop roda até a função do iterador retornar.
Onde a analogia falha: numa máquina de verdade, você aperta o botão e tira o próximo item quando está pronto. Um iterador Go funciona ao contrário. A máquina está no comando. Ela chama você, o corpo do seu loop, uma vez por item, e você só pode responder “mais” ou “pare”. Isso é push, não pull, e é por isso que a função iter.Pull existe, como mostra a última seção.
break faz yield devolver false
Observar o próprio iterador mostra o que o false faz. Aqui ele imprime uma linha antes de cada valor, e outra quando para:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
fmt.Println("iterator: sending", i)
if !yield(i) {
fmt.Println("iterator: loop said stop")
return
}
}
fmt.Println("iterator: ran out")
}
}
func main() {
for n := range Countdown(5) {
fmt.Println("loop: got", n)
if n == 4 {
break
}
}
fmt.Println("after the loop")
}
Ele imprime:
iterator: sending 5
loop: got 5
iterator: sending 4
loop: got 4
iterator: loop said stop
after the loop
O iterador enviou 5 e recebeu true de volta. Enviou 4, e o corpo do loop chegou ao break, então aquela chamada a yield devolveu false. O iterador imprimiu a mensagem dele e retornou, e só então after the loop rodou. Ele nunca calculou 3, 2 ou 1.
Se um iterador ignora o false e continua chamando yield, o Go não roda o corpo do seu loop de novo em silêncio. Ele dá panic:
package main
import "fmt"
func Countdown(n int) func(func(int) bool) {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
yield(i)
}
}
}
func main() {
for n := range Countdown(3) {
fmt.Println(n)
if n == 2 {
break
}
}
}
Ele imprime duas linhas e depois para:
3
2
panic: runtime error: range function continued iteration after function for loop body returned false
Esta versão também mostra que iter.Seq é só um nome. Um func(func(int) bool) simples funciona do mesmo jeito em um range loop.
Iteradores que recebem iteradores
Como um iterador é um valor, uma função pode receber um e devolver outro. Filter repassa só os valores que passam num teste:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func Filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
func main() {
even := func(n int) bool { return n%2 == 0 }
for n := range Filter(Countdown(10), even) {
fmt.Println(n)
}
}
Ele imprime:
10
8
6
4
2
Filter é genérica, então funciona com uma sequência de qualquer coisa. Por dentro, ela percorre a sequência de entrada com um loop comum. Quando quem chamou Filter para, yield devolve false, Filter retorna, e isso encerra também o loop interno dela, o que por sua vez faz o yield de Countdown devolver false. Um break no topo desce por toda a cadeia.
Dois valores por passo: iter.Seq2
Algumas sequências entregam pares naturalmente, como um índice e um valor, ou uma chave e um valor. Para essas, iter.Seq2[K, V] é func(yield func(K, V) bool), e o range loop recebe duas variáveis. Os pacotes slices e maps devolvem os dois tipos:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
fruits := []string{"pear", "apple", "fig"}
for i, f := range slices.All(fruits) {
fmt.Println(i, f)
}
for f := range slices.Values(fruits) {
fmt.Println(strings.ToUpper(f))
}
stock := map[string]int{"pear": 3, "apple": 0, "fig": 12}
names := slices.Collect(maps.Keys(stock))
fmt.Println(len(names))
fmt.Println(slices.Sorted(maps.Keys(stock)))
}
Ele imprime:
0 pear
1 apple
2 fig
PEAR
APPLE
FIG
3
[apple fig pear]
slices.All é um iter.Seq2[int, string]: índice e valor, como percorrer o próprio slice. slices.Values é um iter.Seq[string] só com os valores. maps.Keys é um iter.Seq[string] sobre as chaves, na ordem imprevisível de sempre do map, e é por isso que o programa imprime só quantas chaves slices.Collect juntou, e não o slice em si.
Essa última linha é a que você viu nas partes sobre fluxo de controle e maps. Agora você consegue ler a linha inteira. maps.Keys(stock) não monta um slice. Ele devolve um iterador. slices.Sorted percorre esse iterador, junta cada valor num slice novo, ordena e devolve esse slice. slices.Sorted recebe um iter.Seq[E] em que E é cmp.Ordered, então as duas metades deste post se encontram numa só linha.
Percorrer um slice ou um map diretamente continua sendo o jeito normal. Essas funções mostram o seu valor quando você quer passar uma sequência para outra coisa, como slices.Sorted, ou para o seu próprio Filter.
Puxando valores com iter.Pull
Às vezes você quer mesmo apertar o botão por conta própria, um valor de cada vez, fora de um loop. iter.Pull transforma um iterador push em uma função next e uma função stop:
package main
import (
"fmt"
"iter"
)
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
next, stop := iter.Pull(Countdown(2))
defer stop()
for range 3 {
v, ok := next()
fmt.Println(v, ok)
}
}
Ele imprime:
2 true
1 true
0 false
Cada chamada a next devolve o próximo valor e true, ou o zero value e false quando o iterador terminou. A terceira chamada recebe 0 false, o mesmo formato comma-ok de Stack.Pop.
Chamar stop avisa ao iterador que você terminou, então o yield dele devolve false e ele pode fazer a limpeza. defer stop() garante que isso aconteça mesmo se você parar de puxar antes. Você recorre a iter.Pull quando precisa avançar por duas sequências lado a lado, por exemplo para compará-las, algo que um único range loop não consegue fazer.
O que lembrar
- Um type parameter deixa você escrever uma função ou um tipo uma vez para muitos tipos. O compilador normalmente o infere dos argumentos. Quando não consegue, escreva entre colchetes:
Zero[int](). - Uma constraint é o conjunto de tipos permitidos, e decide o que o corpo pode fazer. Use
any,comparableoucmp.Orderedantes de inventar a sua. ~float64quer dizer qualquer tipo cujo underlying type sejafloat64. Sem o til, o seutype Celsius float64fica de fora.- Métodos não podem ter type parameters próprios. Quando o corpo chama métodos em um valor, uma interface costuma ser a escolha mais simples.
- Um iterador é uma função que chama
yielduma vez por valor. Quandoyielddevolvefalse, o loop parou, e o iterador precisa retornar. maps.Keyseslices.Alldevolvem iteradores, eslices.Collecteslices.Sortedtransformam um iterador de volta em um slice.
Um iterador não entrega uma lista. Ele chama você uma vez para cada valor, até você dizer pare.