sync.WaitGroup Errgroup

🏁 Headstart en Concurrency en Go: Demuestra Dominio sobre Sync.WaitGroup y Errgroup

🎈 Introducción

¡Hola, Gophers! ¿Alguna vez has estado en una situación en la que necesitas la ejecución concurrente de varias goroutines pero también tienes que esperar hasta que todas hayan terminado su trabajo antes de proceder? Si es así, entonces estás en el lugar correcto. En este artículo, profundizaremos en dos herramientas poderosas en la biblioteca estándar de Go que son Sync.WaitGroup y ErrGroup. Ambos son excelentes para coordinar el flujo de control entre goroutines concurrentes, pero cada uno tiene sus propias ventajas únicas. Al final de este artículo, no solo entenderás cómo usar estos dos, sino también cuándo usar uno sobre el otro. Al navegar por estas aguas, nos basaremos en ejemplos de la vida real y buscaremos oportunidades para optimizar y manejar errores de manera eficiente.

🔬 Conceptos Teóricos

Tanto Sync.WaitGroup como ErrGroup son primitivas de coordinación en Go para la concurrencia y paralelismo. Ambos ayudan a lidiar con un problema común en la programación concurrente: esperar a que varias goroutines se completen antes de proceder.

Sync.WaitGroup es bastante simple. Incrementas un contador al inicio de una goroutine, y la avalancha de ejecuciones decrementa el contador. El método Wait() bloquea el hilo de ejecución hasta que el contador es cero. Sin embargo, no proporciona un medio para recopilar errores retornados por las goroutines.

Por otro lado, tenemos ErrGroup, disponible en el paquete golang.org/x/sync/errgroup. ErrGroup tiene un comportamiento similar a WaitGroup pero con un bonus: devuelve el primer error no nil que recibe de una goroutine. En caso de un error, ErrGroup también puede cancelar todas las demás goroutines que se están ejecutando.

💻 Implementación Paso a Paso

Con Sync.WaitGroup

Vamos a jugar con Sync.WaitGroup. Imagina que necesitamos realizar varios pedidos de red simultáneamente y queremos esperar a que todos ellos estén completos antes de seguir con cualquier otro procesamiento. Sabemos que podemos lograr esto lanzando cada solicitud en una goroutine separada y utilizando un WaitGroup para coordinar su finalización.

package main

import (
	"fmt"
	"net/http"
	"sync"
)

func fetchURL(wg *sync.WaitGroup, url string) {
	defer wg.Done()
	resp, err := http.Get(url)
	if err != nil {
		fmt.Printf("Error fetching %s: %v\n", url, err)
		return
	}
	fmt.Printf("%s - status: %s\n", url, resp.Status)
}

func main() {
	var wg sync.WaitGroup
	urls := []string{"http://google.com", "http://yahoo.com", "http://bing.com"}

	for _, url := range urls {
		wg.Add(1)
		go fetchURL(&wg, url)
	}

	wg.Wait()
	fmt.Println("All fetching done")
}

Con ErrGroup

Imagina ahora que estas peticiones son interdependientes y si alguna de ellas falla, queremos abortar todas las demás. Eso es algo un poco más complicado de hacer con un WaitGroup, pero es exactamente en lo que ErrGroup brilla.

package main

import (
	"fmt"
	"net/http"
	"golang.org/x/sync/errgroup"
)

func fetchURL(g *errgroup.Group, url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	fmt.Printf("%s - status: %s\n", url, resp.Status)
	return nil
}

func main() {
	var g errgroup.Group
	urls := []string{"http://google.com", "http://yahoo.com", "http://bing.com"}

	for _, url := range urls {
		url := url
		g.Go(func() error {
			return fetchURL(&g, url)
		})
	}

	if err := g.Wait(); err != nil {
		fmt.Printf("Received error: %v\n", err)
	} else {
		fmt.Println("All fetching done without error")
	}
}

🌎 Casos de Uso Reales

El análisis de grandes cantidades de datos y el trabajo con sistemas distribuidos son dos situaciones en las que puedes encontrar útiles las primitivas del grupo de espera.

Sync.WaitGroup es perfecto para casos en los que deseas lanzar varios hilos de ejecución y luego esperar a que todos ellos terminen de ejecutar antes de seguir con algo más. Un caso de uso real podría ser un sistema que necesita realizar varias tareas de procesamiento por lotes en paralelo y luego desea utilizar los resultados de todas esas tareas para un tipo de análisis final.

En el caso de que uno o más de estos hilos de ejecución pueda encontrar un error y necesites detener el procesamiento en todos los demás hilos de ejecución en ese caso, ErrGroup sería la elección perfecta. Un ejemplo de esto podría ser un sistema de detección y prevención de intrusiones en una red informática. Si una de las goroutines detecta un problema, es posible que quieras detener todas las demás goroutines y limpiar el sistema, protegiendo así la integridad de la red.

🌱 Patrones Avanzados

Mientras que Sync.WaitGroup y ErrGroup cubren muchos de los casos de uso para esperar a que varias goroutines se completen, hay situaciones en las que estas herramientas no son suficientes. En tales situaciones, puedes necesitar crear tus propios primitivos de coordinación.

Por ejemplo, es posible que necesites un mecanismo para esperar a que todas las goroutines finalicen mientras también recopilas todos los errores retornados por las goroutines. Esto es algo que Sync.WaitGroup y ErrGroup por sí mismos no pueden lograr: Sync.WaitGroup no recoge errores y ErrGroup detiene la acumulación de errores en el primer error no nil. Para tal caso de uso, puedes crear tu propia implementación personalizada de un grupo de espera de errores.

🎯 Conclusión y Recursos

Espero que después de leer este artículo, te sientas más cómodo trabajando con Sync.WaitGroup y ErrGroup. Incluso aunque estos dos primitivos parezcan simples, son extremadamente potentes y versátiles, permitiéndote manejar complejos casos de coordinación de concurrencia.

Como próximo paso, te recomendaría mirar el patrón de fan-out y fan-in, en el que partes grandes problemas en goroutines más pequeñas que se pueden ejecutar en paralelo y luego combinas sus resultados.

¡Gracias por poner a prueba tus habilidades de Go conmigo, y nos vemos en la próxima publicación!