El paquete iter introduce los tipos fundamentales iter.Seq e iter.Seq2, que funcionan como abstracciones de tipos funcionales para representar secuencias de datos consumibles mediante la instrucción for-range. Estas definiciones estandarizan la firma de las funciones iteradoras, permitiendo que cualquier estructura de datos personalizada exponga una interfaz de recorrido uniforme y compatible con el ecosistema de Go.
Tradicionalmente, Go carecía de un protocolo de iteración universal, lo que obligaba a los desarrolladores a implementar patrones diversos como cierres (closures) ad-hoc, canales para streaming de datos o métodos basados en punteros de estado. La integración de iter.Seq e iter.Seq2 resuelve esta fragmentación al proporcionar un contrato basado en polimorfismo paramétrico (generics), permitiendo que la biblioteca estándar y las librerías de terceros compartan una semántica común para el procesamiento de flujos de datos.
Abstracción de tipos y firmas funcionales
Desde una perspectiva de implementación, iter.Seq[V] es un alias de tipo para una función con la firma func(yield func(V) bool). En este contexto, el tipo de dato V representa el valor emitido en cada paso del bucle. Por otro lado, iter.Seq2[K, V] extiende esta capacidad para manejar pares, habitualmente utilizados para representar índices y valores o claves y valores en mapas, con la firma func(yield func(K, V) bool).
El underlying type de estos iteradores es puramente funcional. No existe una interfaz con métodos como Next() o HasNext(); en su lugar, Go utiliza la inversión de control. Cuando el compilador encuentra un for-range sobre una función que satisface estas firmas, inyecta el cuerpo del bucle como la función yield. La responsabilidad del iterador es invocar a yield sucesivamente mientras desee continuar la ejecución. Si yield retorna false, implica que el consumidor ha solicitado detener la iteración (por ejemplo, mediante un break), y el iterador debe cesar inmediatamente su operación para respetar el scope del llamador.
package main
import (
"fmt"
"iter"
)
// Fib retorna una secuencia iter.Seq[int] infinita de números de Fibonacci
func Fib() iter.Seq[int] {
return func(yield func(int) bool) {
a, b := 0, 1
for {
if !yield(a) {
return // Detención respetando la señal del consumidor
}
a, b = b, a+b
}
}
}
func main() {
// Consumo de iter.Seq[int] usando for-range
for n := range Fib() {
if n > 10 {
break
}
fmt.Print(n, " ")
}
// Output: 0 1 1 2 3 5 8
}
GoLa adopción progresiva en la stdlib es un pilar fundamental de este cambio. Paquetes como slices y maps han incorporado funciones como slices.All, slices.Values, maps.All y maps.Keys, que retornan estas secuencias estandarizadas. Esto permite, por ejemplo, filtrar o transformar elementos de un slice sin asignar memoria para un nuevo slice intermedio, procesando los datos de forma perezosa (lazy evaluation).
package main
import (
"fmt"
"iter"
)
// MapWrapper provee un iterador de pares sobre un mapa interno
type MapWrapper[K comparable, V any] struct {
data map[K]V
}
func (m MapWrapper[K, V]) All() iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for k, v := range m.data {
if !yield(k, v) {
return
}
}
}
}
func main() {
mw := MapWrapper[string, int]{data: map[string]int{"A": 1, "B": 2}}
for k, v := range mw.All() {
fmt.Printf("%s:%d ", k, v)
}
// Output: A:1 B:2 (el orden puede variar)
}
GoEl comportamiento más contraintuitivo radica en la naturaleza síncrona de la ejecución: a pesar de utilizar callbacks, no hay concurrencia implícita; el iterador y el cuerpo del bucle se ejecutan en la misma goroutine y comparten el mismo stack, lo que mantiene la simplicidad del modelo de memoria de Go.
Fuga de goroutines por invocación manual del iterador
Un comportamiento crítico del compilador ocurre cuando se intenta invocar una función de tipo iter.Seq manualmente fuera de un contexto de for-range. Debido a que estas funciones están diseñadas para entregar el control al yield proporcionado por el compilador, llamar a un iterador pasando un cierre manual requiere una gestión meticulosa de los recursos.
Si un iterador interno lanza una goroutine o abre un recurso que depende de la señal de yield para cerrarse, y el programador invoca la función pero deja de llamar a yield sin que la función retorne, se puede producir un leak de recursos o de memoria. El compilador de Go optimiza el caso de for-range para asegurar que el iterador finalice, pero en ejecuciones manuales, la responsabilidad de agotar el iterador o asegurar su salida recae totalmente en el desarrollador, ya que el mecanismo de interrupción de yield es puramente cooperativo.
Módulo: Iteradores
Artículo número: #63