|1. | Modelo de concurrencia
|2. | Goroutines y canales
|3. | Wait Groups
|4. | Select y Worker pools
|5. | ErrGroup (error groups)
Primero, muchas gracias por leer la última entrega de concurrencia en Golang, en el índice encontrás todos los links para navegar entre los distintos posteos.
Que son los error groups?
Sabemos como son los errores en Go y que una parte importante es el famoso (para bien y mal en partes iguales) if err != nil {}
entonces, si ejecutamos, básicamente, cualquier cosa que devuelva un error
lo vamos a tener que manejar, aunque en un ambiente de múltiples goroutines nos surjen otras preguntas.
- Que pasa si hay un error en una rutina? las siguientes deberían correr o no?
- Supongamos que si corren, como recolecto los errores? un
slice
omap
puede generarpanic
si se accede de forma concurrente.
Pueden ser más también dependiendo el contexto.
La verdad es que esas 2 preguntas, no van a tener una solución nativa, solo workarounds, pero ahi es donde llega error groups
.
El paquete es de de Go, pero de los que tienen X, quiere decir que no es del core, sino que es eXternal. Lo encuentrar aca:
import "golang.org/x/sync/errgroup"
Una vez importado y ya declarado en nuestro go.mod, podemos inicializarlo de dos formas
ctx, _ := context.WithTimeout(context.Background(), 300*time.Millisecond)
g, ctx := errgroup.WithContext(ctx)
Para usar un contexto cancelable en X tiempo.
O sino, directamente la estructura (o su dirección de memoria)
eg := &errgroup.Group{}
Ahora, solo tenemos que identificar el trabajo que queremos realizar de forma concurrente y luego lanzar las rutinas, pero ahora, a través de los error groups.
Suponiendo que tengo unos documentos que quiero enviar a un servicio externo para que sean procesados.
docs, err := service.GetDocs()
for doc, _ := range docs {
// 'g' declarado antes
g.Go(func() error {
result, err := service.ProcessDoc(doc)
return nil
})
}
Vemos que la forma de lanzar la rutina es diferente y ya no usamos la palabra reservada 'go'
.
Como argumento, tiene una closure que devuelve un error, ahi es donde podemos manejarlo como mas nos convenga, sin la necesidad de tener que ornamentar nuestro código.
Ahora bien, tenemos que hacer algo con los resultados, sino lo perdemos; el alcance (o scope) de la función hace que no podamos accederlo desde "afuera" de la rutina.
La solución obvia es usar canales, que ya los estudiamos en nuestra lista. Para que sea "thread safe", vamos a usar otro método de los errorgroups. Pasamos al código.
resChan := make(chan DocResult)
docs, err := service.GetDocs()
for doc, _ := range docs {
// 'g' declarado antes
g.Go(func() error {
result, err := service.ProcessDoc(doc)
if err != nil {
return err
}
resChan <- result
return nil
})
}
go func() {
if err := g.Wait(); err != nil { // devuelve el primer err != nil
// handle err
}
close(resChan) // cerramos el canal
}()
El método Wait()
va a esperar que todas las funciones que llamemos con g.Go()
terminen. En caso de que ninguna tenga errores, siempre va a devolver nil
, sino el que tengamos en nuestro servicio.
No olviden cerrar el canal una vez que no vamos a escribir mas en él, para evitar memory leaks.
Como nota, aca no lo repasamos, pero el contexto al ser carrier de una cancelación subyacente, podemos manejarlo también con un select
para que, si desde algún lado lo dan de baja, no sigamos procesando.
Conclusiones
Vemos que es mas sencillo y natural (por así decirlo) manejar rutinas y errores con errGroup. Si bien no tiene todo resuelto en su propio mecanismo, es de fácil acople con otras soluciones como canales, select y demás para que tengamos a disposición todo su potencial. Es muy útil para pipelines o flujos mas complejos, fácil de leer y hacer debug.
Podemos encontrar muchas similitudes con los wait groups y está muy bien, ya que si son parecidos en su objetivo y semántica. Los casos de uso, por el contrario, podemos diferenciarlos y ahi tener mas herramientas a la hora de elegir una solución.
Espero que les guste y lo puedan usar en sus próximos proyectos.
Top comments (0)