Translate

viernes, 2 de enero de 2026

Métodos Monádicos en Go — Parte 2


En el post anterior definimos Result[T], un tipo genérico para representar operaciones que pueden fallar, junto con las funciones Map, FlatMap y OrElse.

Ahora vamos a llevar ese patrón al mundo concurrente, donde los errores y la sincronización suelen volverse un caos si no se estructuran bien.

Go facilita lanzar tareas concurrentes con goroutines, pero mezclar concurrencia y manejo de errores puede ser tedioso:


go func() {

    v, err := doSomething()

    if err != nil {

        // manejar error...

    }

    res, err := doSomethingElse(v)

    if err != nil {

        // otro error...

    }

    // ...

}()


Cada paso necesita su if err != nil, y los canales deben sincronizar resultados y errores manualmente.

Podemos crear una versión concurrente del patrón Result, donde cada operación devuelve un canal que emite un Result[T].


func Async[T any](f func() Result[T]) <-chan Result[T] {

    ch := make(chan Result[T], 1)

    go func() {

        defer close(ch)

        ch <- f()

    }()

    return ch

}


Creamos versiones asíncronas de Map y FlatMap:


func MapAsync[A, B any](in <-chan Result[A], f func(A) B) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        ch <- Map(r, f)

    }()

    return ch

}


func FlatMapAsync[A, B any](in <-chan Result[A], f func(A) <-chan Result[B]) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        if r.Err != nil {

            ch <- Err[B](r.Err)

            return

        }

        out := f(r.Value)

        ch <- <-out

    }()

    return ch

}


Supongamos que tenemos operaciones concurrentes que pueden fallar:


func FetchData() Result[int] {

    time.Sleep(time.Millisecond * 100)

    return Ok(10)

}


func Compute(x int) Result[int] {

    return Ok(x * 2)

}


func SaveResult(x int) Result[string] {

    if x > 15 {

        return Ok(fmt.Sprintf("Saved %d", x))

    }

    return Err[string](fmt.Errorf("too small"))

}


Podemos encadenarlas de forma limpia:


result := FlatMapAsync(

    MapAsync(Async(FetchData), Compute),

    func(x int) <-chan Result[string] { return Async(func() Result[string] { return SaveResult(x) }) },

)


fmt.Println(OrElse(<-result, "failed")) // "Saved 20"

Las ventajas son :

  • Las tareas se ejecutan en goroutines separadas.
  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • No hay if err != nil, ni sincronización manual.


Podés crear fácilmente un combinador All para ejecutar varias tareas concurrentes que devuelvan Result:


func All[T any](tasks ...func() Result[T]) Result[[]T] {

    ch := make(chan Result[T], len(tasks))

    for _, f := range tasks {

        go func(fn func() Result[T]) { ch <- fn() }(f)

    }


    var results []T

    for i := 0; i < len(tasks); i++ {

        r := <-ch

        if r.Err != nil {

            return Err[[]T](r.Err)

        }

        results = append(results, r.Value)

    }

    return Ok(results)

}


Si alguna tarea falla, el error se devuelve inmediatamente.

Si todas tienen éxito, se devuelven los resultados combinados.


El enfoque monádico aplicado a Go te permite combinar funciones puras y goroutines sin perder control del flujo ni del manejo de errores.

Aporta claridad a sistemas concurrentes y simplifica el código de coordinación.