Iteradores estándar con el paquete iter en Go 1.23

El paquete iter [disponible desde Go 1.23] introduce una forma nativa y estandarizada de definir iteradores en Go mediante los tipos iter.Seq[V] y iter.Seq2[K, V]. Un iter.Seq[V] no es una colección, sino una función que acepta una función de callback, denominada yield, la cual es invocada por el iterador para entregar cada valor. Si yield retorna false, el iterador debe detenerse inmediatamente, señalando que el consumidor ha ejecutado un break.

Esta arquitectura de “push-based iteration” (donde el iterador empuja valores hacia el callback) permite una evaluación perezosa (lazy evaluation). En lugar de que una función devuelva un []T completo, lo cual obliga a una asignación en el heap y una copia de todos los elementos, la función devuelve un iterador que calcula o recupera el siguiente elemento solo cuando el bucle for range lo solicita. Esto es fundamental para diseñar APIs de alto rendimiento que trabajen con grandes volúmenes de datos o estructuras complejas (como árboles o grafos) sin disparar la presión sobre el Garbage Collector mediante colecciones intermedias.

Esta técnica es ideal cuando necesitas transformar, filtrar o transformar colecciones sin la penalización de $O(n)$ en memoria adicional. Sin embargo, si implementas un iterador y olvidas propagar el valor de retorno de yield, el consumidor perderá la capacidad de interrumpir la iteración mediante un break, lo que puede causar efectos secundarios inesperados o procesamiento innecesario.

package main

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

type User struct {
	ID   int
	Name string
	Role string
}

// FilterAdmins devuelve un iterador que solo entrega usuarios con rol "Admin".
// No se crea una nueva slice; la filtración ocurre durante la iteración.
func FilterAdmins(users []User) iter.Seq[User] {
	return func(yield func(User) bool) {
		for _, u := range users {
			if u.Role == "Admin" {
				// Es crucial verificar el retorno de yield para respetar el 'break' del consumidor.
				if !yield(u) {
					return
				}
			}
		}
	}
}

// MapNames transforma un iterador de User en un iterador de strings con los nombres.
// Esto demuestra la composición de iteradores sin asignar memoria intermedia.
func MapNames(users iter.Seq[User]) iter.Seq[string] {
	return func(yield func(string) bool) {
		for u := range users {
			if !yield(u.Name) {
				return
			}
		}
	}
}

func main() {
	users := []User{
		{ID: 1, Name: "Alice", Role: "Admin"},
		{ID: 2, Name: "Bob", Role: "User"},
		{ID: 3, Name: "Charlie", Role: "Admin"},
		{ID: 4, Name: "Diana", Role: "User"},
	}

	// 1. Composición: FilterAdmins -> MapNames
	// El pipeline es totalmente lazy. Solo se procesan los Admins.
	fmt.Println("--- Lista de Admins (Pipeline Lazy) ---")
	adminNames := MapNames(FilterAdmins(users))

	for name := range adminNames {
		fmt.Printf("Admin encontrado: %s\n", name)
		// Si aquí hiciéramos un 'break', FilterAdmins dejaría de trabajar gracias al chequeo de yield.
	}

	// 2. Uso de la stdlib con iteradores
	// maps.All retorna un iter.Seq2[K, V]
	rolesMap := map[string]string{
		"owner": "owner_id_99",
		"staff": "staff_id_01",
	}

	fmt.Println("\n--- Mapeo de roles (maps.All) ---")
	for role, id := range maps.All(rolesMap) {
		fmt.Printf("Role: %s, ID: %s\n", role, id)
	}

	// 3. slices.All para iteración directa
	fmt.Println("\n--- Slices (slices.All) ---")
	for i, u := range slices.All(users) {
		fmt.Printf("[%d] %s\n", i, u.Name)
	}
}

Desglose técnico del ejemplo

En la función FilterAdmins, observamos que el retorno es una función anónima que encierra el slice users en un closure. Cuando ejecutas el for range en main, el runtime de Go llama a esa función anónima pasándole el callback yield.

La magia de la composición ocurre en MapNames. Esta función no recibe un []User, sino un iter.Seq[User]. Al iterar sobre users dentro de MapNames, estamos en realidad llamando a la función yield que el main proporcionó, pero envuelta en una nueva capa. Esto crea una cadena de llamadas donde cada elemento fluye a través de las funciones sin haber sido nunca almacenado en una estructura de datos intermedia.

En el bloque de main, maps.All y slices.All demuestran cómo la biblioteca estándar se ha actualizado para integrarse con este modelo. maps.All utiliza iter.Seq2 para permitir el acceso tanto a la clave como al valor en un solo paso, optimizando el acceso a la tabla de hash sin necesidad de extraer todas las claves o valores por separado.

El error frecuente

Un error crítico al implementar iter.Seq es ignorar el valor booleano que devuelve yield. Si el consumidor decide detener la iteración (por ejemplo, usando un break dentro del for range), la función yield devolverá false. Si tu implementación no verifica este retorno y continúa el bucle, el iterador ignorará la señal de parada del consumidor.

// ERROR: El consumidor no puede detener este iterador
func BadFilter(nums []int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, n := range nums {
            // ERROR: Se ignora el retorno de yield. 
            // El consumidor no puede hacer 'break'.
            yield(n) 
        }
    }
}

// Uso que causaría problemas si BadFilter tuviera efectos secundarios (como logs o contadores)
for n := range BadFilter(largeSlice) {
    if n > 10 {
        break // El break solo detiene el loop en main, pero BadFilter sigue ejecutándose en segundo plano
    }
}

Si yield(n) no se evalúa (ej. if !yield(n) { return }), el iterador seguirá iterando sobre el slice original, desperdiciando ciclos de CPU y, lo que es peor, violando el contrato de control de flujo del lenguaje.

236

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio