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.
