Cómo implementar iteradores personalizados en Go 1.23

La implementación de un iterador personalizado en Go se basa en el patrón de inversión de control, donde una función productora acepta un callback de consumo denominado yield. Este mecanismo permite que las estructuras de datos definidas por el usuario se integren nativamente con la instrucción for-range, exponiendo una secuencia de valores sin comprometer la encapsulación de su estado interno.

Este comportamiento se introdujo para resolver la fragmentación en la forma de recorrer colecciones complejas. Históricamente, Go obligaba a elegir entre exponer slices internos (rompiendo la encapsulación), usar canales (con el overhead de programación concurrente) o implementar métodos Next() manuales propensos a errores de estado. Con la unificación en el paquete iter, el lenguaje proporciona una semántica clara donde el scope de la iteración queda confinado a la ejecución de una función, garantizando que el ciclo de vida de los datos sea predecible y eficiente.

El contrato funcional de yield y el control de flujo

Desde el punto de vista del sistema de tipos, un iterador es una función cuyo underlying type coincide con func(yield func(T) bool) bool. La clave de este patrón reside en la función yield: el compilador transforma el bloque de código dentro de nuestro for-range en el cuerpo de esta función. La assignability del iterador depende de que su firma respete la capacidad de recibir este callback y retornar un booleano que indique si la secuencia se completó o fue interrumpida.

Cuando el iterador invoca a yield(value), está transfiriendo temporalmente el control al cuerpo del bucle. El valor de retorno de yield es la señal de control más crítica:

  • Si yield retorna true, el bucle continúa normalmente.
  • Si yield retorna false, significa que el consumidor ha ejecutado un break, return o ha sufrido un pánico, y el iterador debe cesar su ejecución inmediatamente.
package main

import (
	"fmt"
	"iter"
)

// RangeStep genera una secuencia de enteros con un incremento específico
func RangeStep(start, stop, step int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for v := start; v <= stop; v += step {
			// Si el bucle consumidor hace break, yield devuelve false
			if !yield(v) {
				return // Finalización limpia de la función iteradora
			}
		}
	}
}

func main() {
	// Consumo de la función iteradora personalizada
	for n := range RangeStep(0, 10, 2) {
		if n > 5 {
			break
		}
		fmt.Print(n, " ")
	}
	// Output: 0 2 4
}
Go

Es fundamental entender que el iterador tiene la responsabilidad total de realizar cualquier limpieza de recursos (como cerrar archivos o liberar mutexes) justo después de detectar que yield ha devuelto false. Ignorar este valor de retorno y continuar llamando a yield después de recibir un false se considera un error de implementación que puede derivar en comportamientos indefinidos en el runtime.

Gestión de cierres diferidos y el estado de retorno de yield

Un aspecto avanzado al implementar un iterador personalizado es cómo interactúan las sentencias defer con la interrupción del bucle. Dado que el iterador es técnicamente una función que se ejecuta en el mismo stack que el llamador, el uso de defer es la forma idiomática y segura de garantizar que los recursos se liberen, independientemente de si la iteración termina por agotamiento de datos o por una interrupción del consumidor.

// Ejemplo de iterador con gestión de recursos
func ResourceIterator(res *Resource) iter.Seq[string] {
	return func(yield func(string) bool) {
		res.Open()
		defer res.Close() // Se garantiza la ejecución tras el break del loop

		for res.HasNext() {
			if !yield(res.Next()) {
				return // Sale y dispara el defer
			}
		}
	}
}
Go

El comportamiento más contraintuitivo ocurre cuando se intenta reutilizar el valor de retorno del iterador. Aunque la firma func(yield func(T) bool) bool permite devolver un booleano al finalizar la función, el compilador de Go, al usar for-range, ignora este valor de retorno final. Solo el booleano devuelto por cada invocación individual de yield afecta el flujo del programa.

Fugas de ejecución por omisión del valor de retorno de yield

Un edge case crítico sucede cuando el programador olvida verificar el resultado de yield dentro de un bucle infinito o una operación de larga duración. Si el iterador continúa procesando datos después de que el consumidor ha ejecutado un break, la lógica del iterador seguirá consumiendo ciclos de CPU innecesariamente hasta que su propia lógica interna decida finalizar.

En Go 1.23, no existe un mecanismo de “muerte forzada” para el iterador desde el exterior; es una cooperación obligatoria. Si tu iterador está realizando una operación de red o un escaneo de base de datos pesado y no respeta el false de yield, estarás creando una fuga de ejecución que mantendrá activa la goroutine actual mucho más tiempo de lo esperado, retrasando la recolección de basura de todos los objetos capturados en el cierre (closure) del iterador.


  • Módulo: Iteradores
  • Artículo número: #64

Dejar un comentario

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

Scroll al inicio