Iteradores personalizados con range-over-func en Go 1.23

La introducción de range-over-func [disponible desde Go 1.23] permite definir iteradores personalizados mediante funciones con una firma específica: func(yield func(T) bool). En lugar de devolver una colección completa —como un slice— que debe residir en memoria, devuelves una función de orden superior que el bucle for range invoca. Esta mecánica funciona mediante un modelo de “pull” donde el runtime de Go actúa como director de orquesta: el bucle for llama a tu función y esta, a su vez, llama a yield para entregar cada elemento. Si el cuerpo del bucle contiene un break, un return o un continue, la función yield devolverá false, indicando al iterador que debe detenerse inmediatamente. Es el patrón ideal para recorrer estructuras no lineales (como árboles o grafos) o para implementar paginación de bases de datos sin necesidad de cargar miles de registros en un slice de memoria, optimizando drásticamente el memory footprint. Sin embargo, si ignoras el valor de retorno de yield, el iterador continuará ejecutando su lógica interna incluso cuando el consumidor haya abandonado el bucle, desperdiciando ciclos de CPU y violando el contrato de control de flujo.

package main

import (
	"fmt"
	"iter" // [disponible desde Go 1.23]
)

// Node representa un nodo en una estructura de árbol.
type Node struct {
	Name     string
	Children []*Node
}

// All implementa un iterador para un recorrido pre-order (raíz, luego hijos).
// Devuelve un iter.Seq[*Node], que es una función con la firma func(func(*Node) bool).
func (n *Node) All() iter.Seq[*Node] {
	return func(yield func(*Node) bool) {
		// Definimos una función recursiva interna para recorrer el árbol.
		var walk func(*Node) bool
		walk = func(curr *Node) bool {
			if curr == nil {
				return true
			}

			// Llamamos a yield para entregar el nodo actual.
			// Si yield devuelve false, significa que el consumidor 
			// ejecutó un 'break' en el loop 'for range'.
			if !yield(curr) {
				return false
			}

			// Recorremos los hijos recursivamente.
			for _, child := range curr.Children {
				// Si un hijo devuelve false, propagamos el 'false' 
				// para detener toda la recursión.
				if !walk(child) {
					return false
				}
			}
			return true
		}

		walk(n)
	}
}

func main() {
	// Construcción de un árbol de ejemplo.
	tree := &Node{
		Name: "Root",
		Children: []*Node{
			{
				Name: "Branch A",
				Children: []*Node{
					{Name: "Leaf A1"},
					{Name: "Leaf A2"},
				},
			},
			{
				Name: "Branch B",
				Children: []*Node{
					{Name: "Leaf B1"},
					{Name: "Leaf B2"},
				},
			},
		},
	}

	fmt.Println("Iniciando recorrido del árbol:")
	// Usamos el iterador personalizado directamente en el range.
	for node := range tree.All() {
		fmt.Printf("Visitando: %s\n", node.Name)

		// Simulamos una condición de parada: detenerse al encontrar "Branch B".
		if node.Name == "Branch B" {
			fmt.Println("--> Condición de parada detectada (break)")
			break
		}
	}
}

En el ejemplo, la función All devuelve un tipo iter.Seq[*Node], que es la forma estándar de definir iteradores de un solo valor. La lógica central reside en la función anónima que retorna All, la cual encapsula la recursión. Cuando el for range en main comienza, el runtime invoca esa función anónima y le pasa una implementación de yield.

Fíjate en cómo walk gestiona la recursión: cada llamada a yield(curr) es un punto de control crítico. En la línea donde verificamos if !yield(curr), estamos implementando la respuesta al control de flujo del consumidor. Cuando main ejecuta el break al encontrar “Branch B”, la siguiente llamada a yield que ocurra devolverá false. Gracias a que walk comprueba este valor y lo propaga hacia arriba en la pila de llamadas (if !walk(child) { return false }), la recursión se interrumpe limpiamente, permitiendo que la función All termine y el programa continúe sin procesar “Leaf B1” o “Leaf B2”.

El error frecuente

Un error común al implementar iteradores es ignorar el valor de retorno de yield. Esto sucede principalmente en bucles internos dentro de tu iterador.

// Código incorrecto
func (n *Node) BadAll() iter.Seq[*Node] {
	return func(yield func(*Node) bool) {
		for _, child := range n.Children {
			// ERROR: Se ignora el retorno de yield.
			// Si el consumidor hace un 'break', yield devolverá false,
			// pero este bucle continuará con el siguiente hijo.
			yield(child) 
		}
	}
}

Si utilizas el BadAll anterior y el consumidor ejecuta un break, el bucle for range en el código principal se detendrá, pero la función BadAll seguirá intentando procesar todos los demás hijos del nodo raíz en segundo plano. Aunque el programa no colapsará, estarás realizando trabajo computacional innecesario y violando la semántica esperada de un iterador, lo que puede derivar en efectos secundarios indeseados si el iterador gestiona recursos externos o estados mutables.

235

Dejar un comentario

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

Scroll al inicio