Los genéricos de Go permiten que una función sirva para muchos tipos, con restricciones que dicen qué tipos se aceptan. Los iteradores permiten que una función entregue valores de uno en uno a un bucle for, y se detenga en cuanto el bucle hace break.
Los genéricos y los iteradores llegaron a Go con algunos años de diferencia, pero van juntos en un solo post. Los genéricos te permiten escribir una función o un tipo una vez y usarlo con muchos tipos. Los iteradores, construidos sobre ellos, permiten que un bucle for ... range común recorra cualquier cosa que puedas describir con una función.
Este post empieza con el problema que resuelven los genéricos, recorre las restricciones y los tipos genéricos, y luego construye iteradores desde cero antes de usar los de la biblioteca estándar. Cada programa de abajo se ejecutó en Go 1.26, y su salida está copiada de esa ejecución.
El problema: la misma función, dos veces
Sin genéricos, una función que suma números tiene que elegir un solo tipo numérico. Si la necesitas para int y para float64, la escribes dos veces:
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}))
}
Imprime:
6
4
Los dos cuerpos son idénticos. Solo cambió el tipo. Cada corrección que haces en uno la tienes que acordarte de hacer en el otro, y un tercer tipo numérico significa una tercera copia.
Una función con un parámetro de tipo
Desde Go 1.18, una función puede recibir un tipo como parámetro, escrito entre corchetes antes de los parámetros normales:
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}))
}
Imprime:
6
4
0.75
T es el parámetro de tipo. int | float64 es su restricción: el conjunto de tipos que T puede ser. Dentro de la función, T funciona como cualquier otro nombre de tipo, así que var total T declara un valor cero del tipo que resulte ser T.
En las dos primeras llamadas no dijiste qué era T. El compilador miró el argumento, vio un []int y dedujo que T tiene que ser int. Eso es la inferencia de tipos. La tercera llamada nombra el tipo de forma explícita, Sum[float64], y eso se llama instanciación. Aquí sobra, pero a veces es la única opción.
Explicado como si tuvieras diez años
Un cortador de galletas hace la misma forma con cualquier masa en la que lo aprietes: de chocolate, simple, de jengibre. No necesitas un cortador de estrella distinto para cada tipo de masa.
Pero solo funciona con masa. Apriétalo contra una piedra y no pasa nada. La restricción es la etiqueta del cortador que dice “funciona con masa”.
La versión precisa
Una función genérica es una plantilla que el compilador comprueba una vez, contra su restricción. En cada llamada, el argumento de tipo se deduce de los argumentos normales o se da entre corchetes. Después, el compilador se asegura de que ese tipo esté en el conjunto de tipos de la restricción. Dentro del cuerpo solo puedes hacer lo que permite cada tipo del conjunto: aquí + funciona con T porque tanto int como float64 lo admiten.
Dónde falla la analogía: un cortador de galletas hace una forma con masa mezclada, pero una función genérica nunca mezcla tipos en una llamada. Sum sobre un []int devuelve un int. No le puedes pasar un slice que tenga ints y floats a la vez, porque Go no tiene ese tipo de slice.
Cuando la inferencia no funciona
El compilador deduce un parámetro de tipo solo a partir de los argumentos que pasas. Si T aparece solo en el resultado, no hay nada de dónde deducirlo:
package main
import "fmt"
func Zero[T any]() T {
var z T
return z
}
func main() {
x := Zero()
fmt.Println(x)
}
La compilación falla con:
./main.go:11:11: in call to Zero, cannot infer T
Go no mira cómo usas el resultado para adivinar. Tienes que nombrar el 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]())
}
Imprime:
0 "" false
Cada instanciación da el valor cero de su propio tipo. Te vas a encontrar con la misma situación en los tipos genéricos, donde Stack[int]{} tampoco tiene argumentos de dónde deducir.
Restricciones: any, comparable y cmp.Ordered
Una restricción es una interfaz, y decide qué operaciones puede usar el cuerpo de la función. La más amplia es any, que acepta todos los tipos y por eso casi no permite operaciones. Ni siquiera puedes comparar dos valores any con ==, porque algunos tipos, como los slices, no se pueden comparar:
package main
import "fmt"
func Equal[T any](a, b T) bool {
return a == b
}
func main() {
fmt.Println(Equal(1, 1))
}
La compilación falla con:
./main.go:6:9: invalid operation: a == b (incomparable types in type set)
Dos restricciones de la biblioteca estándar cubren los casos comunes. comparable acepta todos los tipos que admiten == y !=. cmp.Ordered, del paquete cmp que llegó en Go 1.21, acepta todos los tipos que admiten < y >: los enteros, los flotantes y los 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"}))
}
Imprime:
2
-1
16
pear
Elige la restricción más estrecha que le permita al cuerpo hacer su trabajo. Index solo necesita ==, así que recibe comparable y también funciona con structs. Largest necesita >, así que recibe cmp.Ordered. La biblioteca estándar ya tiene los dos trabajos como slices.Index y slices.Max, con estas mismas restricciones.
Restricciones de unión y qué significa ~
Puedes darle nombre a tu propia restricción como una interfaz que lista tipos, lo que queda más ordenado que escribir int | float64 en cada función. Hay una trampa con los tipos que defines tú. Este parece que debería 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))
}
La compilación falla con:
./main.go:21:17: Celsius does not satisfy Number (possibly missing ~ for float64 in Number)
Celsius está construido sobre float64, pero es un tipo distinto. Un tipo que declaras es distinto del tipo del que está hecho, aunque guarde los mismos valores. El conjunto de tipos int | float64 tiene exactamente dos tipos, y Celsius no es uno de ellos.
El compilador hasta te dice cómo arreglarlo. Una ~ antes de un tipo significa “este tipo, o cualquier tipo cuyo tipo subyacente sea 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)
}
Imprime:
64 main.Celsius
Ahora ~float64 incluye float64, Celsius y cualquier otro tipo declarado como type X float64. El resultado sigue siendo un Celsius, no un float64 común, así que conservas el significado de tu tipo. Por eso cmp.Ordered está escrito con tildes en cada línea: tiene que aceptar tu type UserID int además de int.
Tipos genéricos: un Stack[T]
Los tipos también pueden recibir parámetros de tipo, igual que las funciones. Una pila es el caso clásico, porque la lógica es la misma sin importar lo que 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())
}
Imprime:
2 true
1
"" false
go true
Stack[int] y Stack[string] son dos tipos separados. Cada método escribe Stack[T] en su receptor, y T dentro del método es el tipo con el que se creó la pila. Pop devuelve un par comma-ok, el mismo patrón que usan los maps, así que una pila vacía da 0 false o "" false en lugar de un panic.
Un método no puede declarar sus propios parámetros de tipo, solo usar los del 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]{})
}
La compilación falla con:
syntax error: method must have no type parameters
Cuando necesites eso, escribe una función común: func Map[T, U any](s *Stack[T], f func(T) U) []U.
Cuándo no usar genéricos
Los genéricos son para código que es idéntico entre tipos y solo mueve los valores de un lado a otro: contenedores, Sum, Index, Largest. Cuando el código llama a métodos del valor, una interfaz suele ser más simple.
Piensa en una función que imprime cualquier cosa con un método String. Podrías escribir func Show[T fmt.Stringer](v T). Pero func Show(v fmt.Stringer) hace el mismo trabajo, se lee más fácil y era la forma normal en Go mucho antes de que existieran los genéricos. El parámetro de tipo no aporta nada, porque el cuerpo solo llama a v.String().
Una guía aproximada:
- Si escribirías el mismo cuerpo para varios tipos y solo cambia el nombre del tipo, usa un parámetro de tipo.
- Si cada tipo hace lo suyo detrás de un método, usa una interfaz.
- Si hoy tienes un solo tipo, usa ese tipo. Lo puedes hacer genérico cuando aparezca un segundo.
Iteradores: una función que entrega valores
Desde Go 1.23, un bucle for ... range puede recorrer una función. Una función así se llama iterador. Recibe un argumento, un callback que por convención se llama yield, y lo llama una vez por 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")
}
Imprime:
3
2
1
liftoff
iter.Seq[int] es un tipo genérico del paquete iter. Es solo un nombre para func(yield func(int) bool). Countdown no cuenta nada por sí misma. Devuelve una función que cuenta cuando el bucle se lo pide.
Cada vez que el iterador llama a yield(i), el cuerpo del bucle se ejecuta una vez con n igual a i. Cuando la función iteradora retorna, el bucle termina.
Explicado como si tuvieras diez años
Un iterador es una máquina expendedora. Cada vez que aprietas el botón, te entrega un producto. No te tira todo el inventario al piso de una vez, y la máquina no necesita saber cuántos quieres.
También te puedes ir cuando quieras. La máquina nota que nadie aprieta el botón y deja de entregar cosas.
La versión precisa
El cuerpo del bucle se convierte en la función yield. yield devuelve true si el bucle quiere otro valor, y false si el bucle terminó antes, con break, return o un goto hacia fuera del bucle. El iterador tiene que revisar ese bool y retornar en cuanto vea false. El bucle range se ejecuta hasta que la función iteradora retorna.
Dónde falla la analogía: con una máquina expendedora real, tú aprietas el botón y sacas el siguiente producto cuando estás listo. Un iterador de Go funciona al revés. La máquina manda. Te llama a ti, al cuerpo de tu bucle, una vez por producto, y tú solo puedes responder “más” o “basta”. Eso es empujar, no jalar, y es la razón por la que existe la función iter.Pull, como muestra la última sección.
break hace que yield devuelva false
Observar el propio iterador muestra qué hace false. Aquí imprime una línea antes de cada valor, y otra cuando se detiene:
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")
}
Imprime:
iterator: sending 5
loop: got 5
iterator: sending 4
loop: got 4
iterator: loop said stop
after the loop
El iterador envió 5 y recibió true. Envió 4, y el cuerpo del bucle llegó a break, así que esa llamada a yield devolvió false. El iterador imprimió su mensaje y retornó, y solo entonces se ejecutó after the loop. Nunca calculó 3, 2 ni 1.
Si un iterador ignora el false y sigue llamando a yield, Go no vuelve a ejecutar tu cuerpo de bucle en silencio. Provoca un 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
}
}
}
Imprime dos líneas y luego se detiene:
3
2
panic: runtime error: range function continued iteration after function for loop body returned false
Esta versión también muestra que iter.Seq es solo un nombre. Un func(func(int) bool) común funciona igual en un bucle range.
Iteradores que reciben iteradores
Como un iterador es un valor, una función puede recibir uno y devolver otro. Filter deja pasar solo los valores que cumplen una prueba:
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)
}
}
Imprime:
10
8
6
4
2
Filter es genérica, así que funciona con una secuencia de cualquier cosa. Por dentro, recorre la secuencia de entrada con un bucle común. Cuando quien la llama se detiene, yield devuelve false, Filter retorna, y eso también termina su bucle interno, lo que a su vez hace que el yield de Countdown devuelva false. Un break arriba viaja por toda la cadena hasta abajo.
Dos valores por paso: iter.Seq2
Algunas secuencias entregan pares de forma natural, como un índice y un valor, o una clave y un valor. Para esas, iter.Seq2[K, V] es func(yield func(K, V) bool), y el bucle range recibe dos variables. Los paquetes slices y maps devuelven los dos 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)))
}
Imprime:
0 pear
1 apple
2 fig
PEAR
APPLE
FIG
3
[apple fig pear]
slices.All es un iter.Seq2[int, string]: índice y valor, como recorrer el propio slice. slices.Values es un iter.Seq[string] solo con los valores. maps.Keys es un iter.Seq[string] sobre las claves, en el orden impredecible de siempre del map, y por eso el programa imprime solo cuántas claves reunió slices.Collect y no el slice en sí.
Esa última línea es la que viste en las partes sobre control de flujo y maps. Ahora la puedes leer completa. maps.Keys(stock) no construye un slice. Devuelve un iterador. slices.Sorted recorre ese iterador, reúne cada valor en un slice nuevo, lo ordena y lo devuelve. slices.Sorted recibe un iter.Seq[E] donde E es cmp.Ordered, así que las dos mitades de este post se juntan en una línea.
Recorrer un slice o un map directamente sigue siendo la forma normal. Estas funciones se ganan su lugar cuando quieres pasar una secuencia a otra cosa, como slices.Sorted o tu propio Filter.
Jalar valores con iter.Pull
A veces sí quieres apretar el botón tú mismo, un valor a la vez, fuera de un bucle. iter.Pull convierte un iterador que empuja en una función next y una función 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)
}
}
Imprime:
2 true
1 true
0 false
Cada llamada a next devuelve el siguiente valor y true, o el valor cero y false cuando el iterador terminó. La tercera llamada recibe 0 false, la misma forma comma-ok que Stack.Pop.
Llamar a stop le dice al iterador que terminaste, así que su yield devuelve false y puede limpiar. defer stop() se asegura de que eso pase aunque dejes de jalar antes. Recurrirías a iter.Pull cuando necesitas avanzar por dos secuencias en paralelo, por ejemplo para compararlas, algo que un solo bucle range no puede hacer.
Qué recordar
- Un parámetro de tipo te permite escribir una función o un tipo una vez para muchos tipos. Normalmente el compilador lo deduce de los argumentos. Cuando no puede, escríbelo entre corchetes:
Zero[int](). - Una restricción es el conjunto de tipos permitidos, y decide qué puede hacer el cuerpo. Usa
any,comparableocmp.Orderedantes de inventar la tuya. ~float64significa cualquier tipo cuyo tipo subyacente seafloat64. Sin la tilde, tutype Celsius float64queda fuera.- Los métodos no pueden tener sus propios parámetros de tipo. Cuando el cuerpo llama a métodos de un valor, una interfaz suele ser la opción más simple.
- Un iterador es una función que llama a
yielduna vez por valor. Cuandoyielddevuelvefalse, el bucle se detuvo, y el iterador tiene que retornar. maps.Keysyslices.Alldevuelven iteradores, yslices.Collectyslices.Sortedconvierten un iterador de nuevo en un slice.
Un iterador no te entrega una lista. Te llama una vez por cada valor, hasta que dices basta.